2025-11-29 22:04:44 +01:00
#include "server-context.h"
#include "server-common.h"
#include "server-http.h"
#include "server-task.h"
#include "server-queue.h"
#include "common.h"
#include "llama.h"
#include "log.h"
#include "sampling.h"
#include "speculative.h"
#include "mtmd.h"
#include "mtmd-helper.h"
2026-03-06 11:39:26 +01:00
#include <algorithm>
2025-11-29 22:04:44 +01:00
#include <cstddef>
#include <cinttypes>
#include <memory>
2025-12-02 11:38:57 +01:00
#include <filesystem>
2025-11-29 22:04:44 +01:00
// fix problem with std::min and std::max
#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#ifndef NOMINMAX
# define NOMINMAX
#endif
#include <windows.h>
#endif
using json = nlohmann :: ordered_json ;
constexpr int HTTP_POLLING_SECONDS = 1 ;
// state diagram: https://github.com/ggml-org/llama.cpp/pull/9283
enum slot_state {
SLOT_STATE_IDLE ,
2025-12-06 15:54:38 +01:00
SLOT_STATE_WAIT_OTHER , // after assigning a task, but waiting for parent slot to process prompt
SLOT_STATE_STARTED , // after assigning a task and about to process prompt
2025-11-29 22:04:44 +01:00
SLOT_STATE_PROCESSING_PROMPT ,
SLOT_STATE_DONE_PROMPT ,
SLOT_STATE_GENERATING ,
};
enum server_state {
SERVER_STATE_LOADING_MODEL , // Server is starting up, model not fully loaded yet
SERVER_STATE_READY , // Server is ready and model is loaded
};
struct server_slot {
int id ;
// TODO: change to unique_ptrs for consistency:
llama_context * ctx = nullptr ;
// multimodal
mtmd_context * mctx = nullptr ;
common_speculative * spec = nullptr ;
2026-01-10 00:00:41 +01:00
// TODO: move members that belong to the task (such as `generated_text`, `has_new_line`) to task_results_state
// see https://github.com/ggml-org/llama.cpp/pull/18283#issuecomment-3710175837
2025-11-29 22:04:44 +01:00
std :: unique_ptr < const server_task > task ;
std :: unique_ptr < const server_task > task_prev ; // used for debugging
// used to determine the slot that has been used the longest
int64_t t_last_used = - 1 ;
// generation props
int32_t n_ctx = 0 ; // context size per slot
int32_t n_keep = 0 ;
int32_t n_decoded = 0 ;
int32_t n_remaining = - 1 ;
int32_t i_batch = - 1 ;
int32_t n_prompt_tokens_cache = 0 ;
int32_t n_prompt_tokens_processed = 0 ;
size_t last_nl_pos = 0 ;
std :: string generated_text ;
2026-02-18 18:53:37 +01:00
std :: string debug_generated_text ;
2025-11-29 22:04:44 +01:00
llama_tokens generated_tokens ;
2025-12-08 14:35:28 +01:00
// idx of draft tokens in the main batch
// non-empty if we went to evaluate draft tokens
// ref: https://github.com/ggml-org/llama.cpp/pull/17808
std :: vector < int32_t > i_batch_dft ;
2025-11-29 22:04:44 +01:00
std :: vector < completion_token_output > generated_token_probs ;
bool has_next_token = true ;
bool has_new_line = false ;
bool truncated = false ;
stop_type stop ;
std :: string stopping_word ;
// state
slot_state state = SLOT_STATE_IDLE ;
server_prompt prompt ;
void prompt_save ( server_prompt_cache & prompt_cache ) const {
GGML_ASSERT ( prompt . data . size () == 0 );
const size_t cur_size = llama_state_seq_get_size_ext ( ctx , id , 0 );
SRV_WRN ( " - saving prompt with length %d, total state size = %.3f MiB \n " ,
( int ) prompt . tokens . size (), cur_size / ( 1024.0 * 1024.0 ));
auto * cur = prompt_cache . alloc ( prompt , cur_size );
if ( cur == nullptr ) {
return ;
}
llama_state_seq_get_data_ext ( ctx , cur -> data . data (), cur_size , id , 0 );
}
bool prompt_load ( server_prompt_cache & prompt_cache , const server_tokens & tokens ) {
bool res = prompt_cache . load ( prompt , tokens , ctx , id );
if ( ! res ) {
SLT_WRN ( * this , "%s" , "failed to load prompt from cache \n " );
}
return res ;
}
2026-01-15 16:39:17 +02:00
void prompt_clear ( bool allow_processing ) {
if ( ! allow_processing ) {
GGML_ASSERT ( ! is_processing ());
}
SLT_INF ( * this , "clearing prompt with %zu tokens \n " , prompt . tokens . size ());
llama_memory_seq_rm ( llama_get_memory ( ctx ), id , - 1 , - 1 );
prompt . tokens . clear ();
}
2025-11-29 22:04:44 +01:00
std :: vector < common_adapter_lora_info > lora ;
int32_t alora_invocation_start = - 1 ;
// sampling
json json_schema ;
2025-12-14 10:11:13 +02:00
common_sampler_ptr smpl ;
2025-11-29 22:04:44 +01:00
2026-01-10 00:00:41 +01:00
llama_token sampled ; // in speculative mode, this is the last accepted token
2025-12-08 14:35:28 +01:00
llama_tokens drafted ;
2025-11-29 22:04:44 +01:00
// stats
size_t n_sent_text = 0 ; // number of sent text character
int64_t t_start_process_prompt ;
int64_t t_start_generation ;
double t_prompt_processing ; // ms
double t_token_generation ; // ms
2026-01-30 19:46:10 +02:00
std :: function < void ( int /* id_slot */ ) > callback_on_release ;
2025-11-29 22:04:44 +01:00
// Speculative decoding stats
int32_t n_draft_total = 0 ; // Total draft tokens generated
int32_t n_draft_accepted = 0 ; // Draft tokens actually accepted
void reset () {
SLT_DBG ( * this , "%s" , " \n " );
n_prompt_tokens_cache = 0 ;
last_nl_pos = 0 ;
generated_text = "" ;
has_new_line = false ;
truncated = false ;
stop = STOP_TYPE_NONE ;
stopping_word = "" ;
n_sent_text = 0 ;
2025-12-08 14:35:28 +01:00
drafted . clear ();
i_batch_dft . clear ();
2025-11-29 22:04:44 +01:00
generated_tokens . clear ();
generated_token_probs . clear ();
json_schema = json ();
// clear speculative decoding stats
n_draft_total = 0 ;
n_draft_accepted = 0 ;
2026-01-15 16:39:17 +02:00
task_prev = std :: move ( task );
2025-11-29 22:04:44 +01:00
task . reset ();
2026-01-15 16:39:17 +02:00
llama_set_sampler ( ctx , id , nullptr );
2025-11-29 22:04:44 +01:00
// clear alora start
alora_invocation_start = - 1 ;
}
2026-01-15 16:39:17 +02:00
void init_sampler () const {
common_sampler_reset ( smpl . get ());
if ( ! task -> need_sampling ()) {
return ;
2026-01-10 00:00:41 +01:00
}
const int64_t t_start = ggml_time_us ();
int n_text = 0 ;
for ( int i = 0 ; i < ( int ) prompt . tokens . size (); i ++ ) {
const llama_token id = prompt . tokens [ i ];
if ( id != LLAMA_TOKEN_NULL ) {
common_sampler_accept ( smpl . get (), id , false );
n_text ++ ;
}
}
SLT_INF ( * this , "init sampler, took %0.2f ms, tokens: text = %d, total = %d \n " ,
( ggml_time_us () - t_start ) / 1000.0 , n_text , ( int ) prompt . tokens . size ());
}
2025-11-29 22:04:44 +01:00
// if the context does not have a memory module then all embeddings have to be computed within a single ubatch
// also we cannot split if the pooling would require any past tokens
bool can_split () const {
2026-01-15 16:39:17 +02:00
GGML_ASSERT ( task );
2025-11-29 22:04:44 +01:00
return
2026-01-15 16:39:17 +02:00
! task -> need_embd () ||
2025-11-29 22:04:44 +01:00
( llama_get_memory ( ctx ) && llama_pooling_type ( ctx ) == LLAMA_POOLING_TYPE_LAST );
}
bool can_batch_with ( server_slot & other_slot ) const {
GGML_ASSERT ( task );
return task -> type == other_slot . task -> type && are_lora_equal ( lora , other_slot . lora );
}
bool has_budget ( const common_params & global_params ) {
GGML_ASSERT ( task );
if ( task -> params . n_predict == - 1 && global_params . n_predict == - 1 ) {
return true ; // limitless
}
n_remaining = - 1 ;
if ( task -> params . n_predict != - 1 ) {
n_remaining = task -> params . n_predict - n_decoded ;
} else if ( global_params . n_predict != - 1 ) {
n_remaining = global_params . n_predict - n_decoded ;
}
return n_remaining > 0 ; // no budget
}
bool is_processing () const {
return state != SLOT_STATE_IDLE ;
}
bool can_speculate () const {
2026-01-28 18:42:42 +01:00
return !! spec ;
2025-11-29 22:04:44 +01:00
}
void add_token ( const completion_token_output & token ) {
if ( ! is_processing ()) {
SLT_WRN ( * this , "%s" , "slot is not processing \n " );
return ;
}
2026-01-10 00:00:41 +01:00
2025-11-29 22:04:44 +01:00
generated_token_probs . push_back ( token );
}
2025-12-08 14:35:28 +01:00
int get_n_draft_max () const {
2026-01-10 00:00:41 +01:00
GGML_ASSERT ( task );
2025-12-08 14:35:28 +01:00
if ( ! can_speculate ()) {
return 0 ;
}
// determine the max draft that fits the current slot state
int n_draft_max = task -> params . speculative . n_max ;
// note: slot.prompt is not yet expanded with the `id` token sampled above
// also, need to leave space for 1 extra token to allow context shifts
n_draft_max = std :: min ( n_draft_max , n_ctx - prompt . n_tokens () - 2 );
if ( n_remaining > 0 ) {
n_draft_max = std :: min ( n_draft_max , n_remaining - 1 );
}
SLT_DBG ( * this , "max possible draft: %d \n " , n_draft_max );
if ( n_draft_max < task -> params . speculative . n_min ) {
SLT_DBG ( * this , "the max possible draft is too small: %d < %d - skipping speculative decoding \n " , n_draft_max , task -> params . speculative . n_min );
n_draft_max = 0 ;
}
2026-01-28 18:42:42 +01:00
2025-12-08 14:35:28 +01:00
return n_draft_max ;
}
2025-11-29 22:04:44 +01:00
void release () {
if ( is_processing ()) {
GGML_ASSERT ( task );
SLT_INF ( * this , "stop processing: n_tokens = %d, truncated = %d \n " , prompt . n_tokens (), truncated );
2026-01-10 00:00:41 +01:00
t_last_used = ggml_time_us ();
2025-11-29 22:04:44 +01:00
t_token_generation = ( ggml_time_us () - t_start_generation ) / 1e3 ;
2026-01-10 00:00:41 +01:00
2025-11-29 22:04:44 +01:00
state = SLOT_STATE_IDLE ;
2026-01-10 00:00:41 +01:00
// do not keep context of the child slots - the parent's context is enough
2026-01-15 17:10:28 +01:00
if ( task -> is_child ()) {
2026-01-15 16:39:17 +02:00
prompt_clear ( false );
2026-01-10 00:00:41 +01:00
}
2026-01-15 16:39:17 +02:00
reset ();
2025-11-29 22:04:44 +01:00
callback_on_release ( id );
}
}
result_timings get_timings () const {
result_timings timings ;
timings . cache_n = n_prompt_tokens_cache ;
timings . prompt_n = n_prompt_tokens_processed ;
timings . prompt_ms = t_prompt_processing ;
timings . prompt_per_token_ms = t_prompt_processing / n_prompt_tokens_processed ;
timings . prompt_per_second = 1e3 / t_prompt_processing * n_prompt_tokens_processed ;
timings . predicted_n = n_decoded ;
timings . predicted_ms = t_token_generation ;
timings . predicted_per_token_ms = t_token_generation / n_decoded ;
timings . predicted_per_second = 1e3 / t_token_generation * n_decoded ;
// Add speculative metrics
if ( n_draft_total > 0 ) {
timings . draft_n = n_draft_total ;
timings . draft_n_accepted = n_draft_accepted ;
}
return timings ;
}
size_t find_stopping_strings ( const std :: string & text , const size_t last_token_size , bool is_full_stop ) {
GGML_ASSERT ( task );
size_t stop_pos = std :: string :: npos ;
for ( const std :: string & word : task -> params . antiprompt ) {
size_t pos ;
if ( is_full_stop ) {
const size_t tmp = word . size () + last_token_size ;
const size_t from_pos = text . size () > tmp ? text . size () - tmp : 0 ;
pos = text . find ( word , from_pos );
} else {
// otherwise, partial stop
pos = string_find_partial_stop ( text , word );
}
if ( pos != std :: string :: npos && ( stop_pos == std :: string :: npos || pos < stop_pos )) {
if ( is_full_stop ) {
stop = STOP_TYPE_WORD ;
stopping_word = word ;
has_next_token = false ;
}
stop_pos = pos ;
}
}
return stop_pos ;
}
void print_timings () const {
const double t_prompt = t_prompt_processing / n_prompt_tokens_processed ;
const double n_prompt_second = 1e3 / t_prompt_processing * n_prompt_tokens_processed ;
const double t_gen = t_token_generation / n_decoded ;
const double n_gen_second = 1e3 / t_token_generation * n_decoded ;
SLT_INF ( * this ,
" \n "
"prompt eval time = %10.2f ms / %5d tokens (%8.2f ms per token, %8.2f tokens per second) \n "
" eval time = %10.2f ms / %5d tokens (%8.2f ms per token, %8.2f tokens per second) \n "
" total time = %10.2f ms / %5d tokens \n " ,
t_prompt_processing , n_prompt_tokens_processed , t_prompt , n_prompt_second ,
t_token_generation , n_decoded , t_gen , n_gen_second ,
t_prompt_processing + t_token_generation , n_prompt_tokens_processed + n_decoded );
if ( n_draft_total > 0 ) {
const float draft_ratio = ( float ) n_draft_accepted / n_draft_total ;
2025-12-08 14:35:28 +01:00
SLT_CNT ( * this ,
2025-11-29 22:04:44 +01:00
"draft acceptance rate = %0.5f (%5d accepted / %5d generated) \n " ,
draft_ratio , n_draft_accepted , n_draft_total
);
}
2026-01-28 18:42:42 +01:00
common_speculative_print_stats ( spec );
2025-11-29 22:04:44 +01:00
}
json to_json ( bool only_metrics = false ) const {
json res ;
res = {
{ "id" , id },
{ "n_ctx" , n_ctx },
{ "speculative" , can_speculate ()},
{ "is_processing" , is_processing ()},
};
const auto & ptask = task ? task : task_prev ;
if ( ptask ) {
res [ "id_task" ] = ptask -> id ;
res [ "params" ] = ptask -> params . to_json ( only_metrics );
res [ "next_token" ] = {
{
{ "has_next_token" , has_next_token },
{ "has_new_line" , has_new_line },
{ "n_remain" , n_remaining },
{ "n_decoded" , n_decoded },
}
};
if ( ! only_metrics ) {
res [ "prompt" ] = ptask -> tokens . detokenize ( ctx , true );
2026-02-18 18:53:37 +01:00
res [ "generated" ] = generated_text . empty () ? debug_generated_text : generated_text ;
2025-11-29 22:04:44 +01:00
}
}
return res ;
}
2025-12-06 15:54:38 +01:00
void copy_state_to ( server_slot & other ) const {
2026-01-10 00:00:41 +01:00
GGML_ASSERT ( state == SLOT_STATE_DONE_PROMPT );
llama_memory_seq_rm ( llama_get_memory ( ctx ), other . id , - 1 , - 1 );
llama_memory_seq_cp ( llama_get_memory ( ctx ), id , other . id , - 1 , - 1 );
2025-12-06 15:54:38 +01:00
other . n_decoded = n_decoded ;
other . n_remaining = n_remaining ;
other . i_batch = i_batch ;
2026-01-10 00:00:41 +01:00
other . t_start_process_prompt = t_start_process_prompt ;
other . t_prompt_processing = t_prompt_processing ;
2025-12-06 15:54:38 +01:00
other . n_prompt_tokens_cache = n_prompt_tokens_cache ;
other . n_prompt_tokens_processed = n_prompt_tokens_processed ;
2026-01-10 00:00:41 +01:00
2025-12-06 15:54:38 +01:00
other . prompt = prompt . clone ();
2026-01-10 00:00:41 +01:00
other . init_sampler ();
2025-12-06 15:54:38 +01:00
}
2025-11-29 22:04:44 +01:00
};
//
// server_metrics
//
struct server_metrics {
int64_t t_start = 0 ;
uint64_t n_prompt_tokens_processed_total = 0 ;
uint64_t t_prompt_processing_total = 0 ;
uint64_t n_tokens_predicted_total = 0 ;
uint64_t t_tokens_generation_total = 0 ;
uint64_t n_tokens_max = 0 ;
uint64_t n_prompt_tokens_processed = 0 ;
uint64_t t_prompt_processing = 0 ;
uint64_t n_tokens_predicted = 0 ;
uint64_t t_tokens_generation = 0 ;
uint64_t n_decode_total = 0 ;
uint64_t n_busy_slots_total = 0 ;
void init () {
t_start = ggml_time_us ();
}
void on_prompt_eval ( const server_slot & slot ) {
n_prompt_tokens_processed_total += slot . n_prompt_tokens_processed ;
n_prompt_tokens_processed += slot . n_prompt_tokens_processed ;
t_prompt_processing += slot . t_prompt_processing ;
t_prompt_processing_total += slot . t_prompt_processing ;
n_tokens_max = std :: max ( n_tokens_max , ( uint64_t ) slot . prompt . n_tokens ());
}
void on_prediction ( const server_slot & slot ) {
n_tokens_predicted_total += slot . n_decoded ;
n_tokens_predicted += slot . n_decoded ;
t_tokens_generation += slot . t_token_generation ;
t_tokens_generation_total += slot . t_token_generation ;
}
void on_decoded ( const std :: vector < server_slot > & slots ) {
n_decode_total ++ ;
for ( const auto & slot : slots ) {
if ( slot . is_processing ()) {
n_busy_slots_total ++ ;
}
n_tokens_max = std :: max ( n_tokens_max , ( uint64_t ) slot . prompt . n_tokens ());
}
}
void reset_bucket () {
n_prompt_tokens_processed = 0 ;
t_prompt_processing = 0 ;
n_tokens_predicted = 0 ;
t_tokens_generation = 0 ;
}
};
//
// server_context_impl (private implementation)
//
struct server_context_impl {
2025-12-22 14:23:34 +01:00
friend struct server_context ;
public :
// only use these pointers outside of this class:
// - when not in sleeping state
// - and, with thread-safe APIs (e.g., tokenizer calls)
llama_model * model = nullptr ;
mtmd_context * mctx = nullptr ;
const llama_vocab * vocab = nullptr ;
server_queue queue_tasks ;
server_response queue_results ;
2026-01-19 23:28:01 +01:00
// note: chat_params must not be refreshed upon existing sleeping state
server_chat_params chat_params ;
2025-12-22 14:23:34 +01:00
~ server_context_impl () {
if ( ! sleeping ) {
// destroy() is already called when entering sleeping state
// we don't call it again here to avoid double free
destroy ();
}
}
private :
// note: accessing these fields outside of this class is not thread-safe
// use server_context methods instead
2025-11-29 22:04:44 +01:00
common_params params_base ;
// note: keep these alive - they determine the lifetime of the model, context, etc.
2025-12-14 10:11:13 +02:00
common_init_result_ptr llama_init ;
2025-11-29 22:04:44 +01:00
llama_context * ctx = nullptr ;
llama_batch batch {};
2026-01-28 18:42:42 +01:00
llama_model_ptr model_dft ;
2025-11-29 22:04:44 +01:00
bool add_bos_token = true ;
int32_t n_ctx ; // total context for all clients / slots
// slots / clients
std :: vector < server_slot > slots ;
int slots_debug = 0 ;
std :: unique_ptr < server_prompt_cache > prompt_cache ;
server_metrics metrics ;
2025-12-22 14:23:34 +01:00
json json_webui_settings = json :: object ();
2025-12-17 21:45:45 +01:00
2025-11-29 22:04:44 +01:00
// Necessary similarity of prompt for slot selection
float slot_prompt_similarity = 0.0f ;
2025-12-02 11:38:57 +01:00
std :: string model_name ; // name of the loaded model, to be used by API
2026-02-27 07:05:23 +01:00
std :: set < std :: string > model_aliases ; // additional names for the model
std :: set < std :: string > model_tags ; // informational tags
2025-12-02 11:38:57 +01:00
2025-12-21 02:24:42 +01:00
bool sleeping = false ;
void destroy () {
llama_init . reset ();
ctx = nullptr ;
model = nullptr ;
2025-11-29 22:04:44 +01:00
mtmd_free ( mctx );
2025-12-21 02:24:42 +01:00
mctx = nullptr ;
2025-11-29 22:04:44 +01:00
// Clear any sampling context
for ( server_slot & slot : slots ) {
common_speculative_free ( slot . spec );
slot . spec = nullptr ;
}
llama_batch_free ( batch );
}
2025-12-21 02:24:42 +01:00
void handle_sleeping_state ( bool new_state ) {
GGML_ASSERT ( sleeping != new_state );
if ( new_state ) {
SRV_INF ( "%s" , "server is entering sleeping state \n " );
destroy ();
} else {
SRV_INF ( "%s" , "server is exiting sleeping state \n " );
if ( ! load_model ( params_base )) {
GGML_ABORT ( "failed to reload model after sleeping" );
}
}
sleeping = new_state ;
}
2025-11-29 22:04:44 +01:00
// load the model and initialize llama_context
2025-12-21 02:24:42 +01:00
// this may also be called to resume from sleeping state
2025-11-29 22:04:44 +01:00
bool load_model ( const common_params & params ) {
2025-12-21 02:24:42 +01:00
bool is_resume = sleeping ;
2025-11-29 22:04:44 +01:00
SRV_INF ( "loading model '%s' \n " , params . model . path . c_str ());
params_base = params ;
llama_init = common_init_from_params ( params_base );
2025-12-14 10:11:13 +02:00
model = llama_init -> model ();
ctx = llama_init -> context ();
2025-11-29 22:04:44 +01:00
if ( model == nullptr ) {
SRV_ERR ( "failed to load model, '%s' \n " , params_base . model . path . c_str ());
return false ;
}
vocab = llama_model_get_vocab ( model );
n_ctx = llama_n_ctx ( ctx );
add_bos_token = llama_vocab_get_add_bos ( vocab );
2026-01-28 18:42:42 +01:00
if ( params_base . speculative . has_dft ()) {
SRV_INF ( "loading draft model '%s' \n " , params_base . speculative . mparams_dft . path . c_str ());
const auto & params_spec = params_base . speculative ;
2025-11-29 22:04:44 +01:00
auto params_dft = params_base ;
params_dft . n_parallel = 1 ;
2026-01-28 18:42:42 +01:00
params_dft . n_ctx = params_spec . n_ctx == 0 ? llama_n_ctx_seq ( ctx ) : params_spec . n_ctx ;
params_dft . n_batch = llama_n_ctx_seq ( ctx );
params_dft . devices = params_spec . devices ;
params_dft . model = params_spec . mparams_dft ;
params_dft . n_gpu_layers = params_spec . n_gpu_layers ;
params_dft . cache_type_k = params_spec . cache_type_k ;
params_dft . cache_type_v = params_spec . cache_type_v ;
2025-11-29 22:04:44 +01:00
2026-01-28 18:42:42 +01:00
if ( params_spec . cpuparams . n_threads > 0 ) {
params_dft . cpuparams . n_threads = params_spec . cpuparams . n_threads ;
params_dft . cpuparams_batch . n_threads = params_spec . cpuparams_batch . n_threads ;
}
2025-11-29 22:04:44 +01:00
2026-01-28 18:42:42 +01:00
params_dft . tensor_buft_overrides = params_spec . tensor_buft_overrides ;
2025-11-29 22:04:44 +01:00
2026-01-28 18:42:42 +01:00
auto mparams_dft = common_model_params_to_llama ( params_dft );
2025-11-29 22:04:44 +01:00
2026-01-28 18:42:42 +01:00
model_dft . reset ( llama_model_load_from_file ( params_dft . model . path . c_str (), mparams_dft ));
2025-11-29 22:04:44 +01:00
if ( model_dft == nullptr ) {
2026-01-28 18:42:42 +01:00
SRV_ERR ( "failed to load draft model, '%s' \n " , params_dft . model . path . c_str ());
2025-11-29 22:04:44 +01:00
return false ;
}
2026-01-28 18:42:42 +01:00
params_base . speculative . model_dft = model_dft . get ();
params_base . speculative . cparams_dft = common_context_params_to_llama ( params_dft );
2025-11-29 22:04:44 +01:00
}
std :: string & mmproj_path = params_base . mmproj . path ;
if ( ! mmproj_path . empty ()) {
2025-12-21 02:24:42 +01:00
if ( ! is_resume ) {
mtmd_helper_log_set ( common_log_default_callback , nullptr );
}
2025-11-29 22:04:44 +01:00
mtmd_context_params mparams = mtmd_context_params_default ();
2026-01-28 18:42:42 +01:00
2025-11-29 22:04:44 +01:00
mparams . use_gpu = params_base . mmproj_use_gpu ;
mparams . print_timings = false ;
mparams . n_threads = params_base . cpuparams . n_threads ;
mparams . flash_attn_type = params_base . flash_attn_type ;
2025-12-01 21:32:25 +01:00
mparams . warmup = params_base . warmup ;
2025-11-29 22:04:44 +01:00
mparams . image_min_tokens = params_base . image_min_tokens ;
mparams . image_max_tokens = params_base . image_max_tokens ;
2026-01-28 18:42:42 +01:00
2025-11-29 22:04:44 +01:00
mctx = mtmd_init_from_file ( mmproj_path . c_str (), model , mparams );
if ( mctx == nullptr ) {
SRV_ERR ( "failed to load multimodal model, '%s' \n " , mmproj_path . c_str ());
return false ;
}
SRV_INF ( "loaded multimodal model, '%s' \n " , mmproj_path . c_str ());
if ( params_base . ctx_shift ) {
params_base . ctx_shift = false ;
SRV_WRN ( "%s \n " , "ctx_shift is not supported by multimodal, it will be disabled" );
}
if ( params_base . n_cache_reuse ) {
params_base . n_cache_reuse = 0 ;
SRV_WRN ( "%s \n " , "cache_reuse is not supported by multimodal, it will be disabled" );
}
2026-01-30 18:21:48 +02:00
if ( params_base . speculative . type != COMMON_SPECULATIVE_TYPE_NONE ) {
params_base . speculative . type = COMMON_SPECULATIVE_TYPE_NONE ;
SRV_WRN ( "%s \n " , "speculative decoding is not supported by multimodal, it will be disabled" );
}
2025-11-29 22:04:44 +01:00
}
if ( ! llama_memory_can_shift ( llama_get_memory ( ctx ))) {
if ( params_base . ctx_shift ) {
params_base . ctx_shift = false ;
SRV_WRN ( "%s \n " , "ctx_shift is not supported by this context, it will be disabled" );
}
if ( params_base . n_cache_reuse ) {
params_base . n_cache_reuse = 0 ;
SRV_WRN ( "%s \n " , "cache_reuse is not supported by this context, it will be disabled" );
}
}
// Necessary similarity of prompt for slot selection
slot_prompt_similarity = params_base . slot_prompt_similarity ;
// setup slots
SRV_INF ( "initializing slots, n_slots = %d \n " , params_base . n_parallel );
const int n_ctx_train = llama_model_n_ctx_train ( model );
int n_ctx_slot = llama_n_ctx_seq ( ctx );
if ( n_ctx_slot > n_ctx_train ) {
SRV_WRN ( "the slot context (%d) exceeds the training context of the model (%d) - capping \n " , n_ctx_slot , n_ctx_train );
n_ctx_slot = n_ctx_train ;
}
2025-12-21 02:24:42 +01:00
slots . clear ();
2026-01-10 00:00:41 +01:00
2026-02-06 16:47:22 +02:00
const bool can_spec = common_speculative_is_compat ( ctx );
if ( ! can_spec ) {
SRV_WRN ( "%s" , "speculative decoding not supported by this context \n " );
}
2026-01-15 16:39:17 +02:00
// initialize slots
2025-11-29 22:04:44 +01:00
for ( int i = 0 ; i < params_base . n_parallel ; i ++ ) {
server_slot slot ;
2026-01-28 18:42:42 +01:00
slot . id = i ;
slot . ctx = ctx ;
2025-11-29 22:04:44 +01:00
slot . n_ctx = n_ctx_slot ;
2026-01-28 18:42:42 +01:00
slot . mctx = mctx ;
2025-11-29 22:04:44 +01:00
slot . prompt . tokens . has_mtmd = mctx != nullptr ;
2026-01-28 18:42:42 +01:00
// try speculative decoding
2026-02-06 16:47:22 +02:00
if ( can_spec ) {
2026-01-28 18:42:42 +01:00
slot . spec = common_speculative_init ( params_base . speculative , slot . ctx );
if ( slot . spec ) {
if ( mctx ) {
SRV_ERR ( "%s \n " , "speculative decoding is not supported with multimodal" );
return false ;
}
2026-01-30 18:21:48 +02:00
SLT_INF ( slot , "%s" , "speculative decoding context initialized \n " );
2026-01-28 18:42:42 +01:00
} else {
2026-01-30 18:21:48 +02:00
SLT_INF ( slot , "%s" , "speculative decoding context not initialized \n " );
2025-11-29 22:04:44 +01:00
}
}
SLT_INF ( slot , "new slot, n_ctx = %d \n " , slot . n_ctx );
2026-01-30 19:46:10 +02:00
slot . callback_on_release = [ this ]( int id_slot ) {
queue_tasks . pop_deferred_task ( id_slot );
2025-11-29 22:04:44 +01:00
};
slot . reset ();
slots . push_back ( std :: move ( slot ));
}
{
const char * LLAMA_SERVER_SLOTS_DEBUG = getenv ( "LLAMA_SERVER_SLOTS_DEBUG" );
slots_debug = LLAMA_SERVER_SLOTS_DEBUG ? atoi ( LLAMA_SERVER_SLOTS_DEBUG ) : 0 ;
if ( slots_debug ) {
SRV_WRN ( "slots debug = %d \n " , slots_debug );
}
}
// the update_slots() logic will always submit a maximum of n_batch or n_parallel tokens
// note that n_batch can be > n_ctx (e.g. for non-causal attention models such as BERT where the KV cache is not used)
{
const int32_t n_batch = llama_n_batch ( ctx );
batch = llama_batch_init ( std :: max ( n_batch , params_base . n_parallel ), 0 , 1 );
}
if ( params_base . cache_ram_mib != 0 ) {
if ( params_base . cache_ram_mib < 0 ) {
SRV_WRN ( "prompt cache is enabled, size limit: %s \n " , "no limit" );
} else {
SRV_WRN ( "prompt cache is enabled, size limit: %d MiB \n " , params_base . cache_ram_mib );
}
SRV_WRN ( "%s" , "use `--cache-ram 0` to disable the prompt cache \n " );
prompt_cache = std :: make_unique < server_prompt_cache > ( params_base . cache_ram_mib , n_ctx );
} else {
SRV_WRN ( "%s" , "prompt cache is disabled - use `--cache-ram N` to enable it \n " );
}
SRV_WRN ( "%s" , "for more info see https://github.com/ggml-org/llama.cpp/pull/16391 \n " );
2025-12-02 11:38:57 +01:00
if ( ! params_base . model_alias . empty ()) {
2026-02-27 07:05:23 +01:00
// backward compat: use first alias as model name
model_name = * params_base . model_alias . begin ();
2025-12-02 11:38:57 +01:00
} else if ( ! params_base . model . name . empty ()) {
model_name = params_base . model . name ;
} else {
// fallback: derive model name from file name
auto model_path = std :: filesystem :: path ( params_base . model . path );
model_name = model_path . filename (). string ();
}
2026-02-27 07:05:23 +01:00
model_aliases = params_base . model_alias ;
model_tags = params_base . model_tags ;
2025-12-21 02:24:42 +01:00
if ( ! is_resume ) {
return init ();
}
return true ;
}
// unlike load_model(), this is only called once during initialization
bool init () {
GGML_ASSERT ( ctx != nullptr );
GGML_ASSERT ( model != nullptr );
GGML_ASSERT ( ! sleeping );
// wiring up server queues
queue_tasks . on_new_task ([ this ]( server_task && task ) {
process_single_task ( std :: move ( task ));
});
queue_tasks . on_update_slots ([ this ]() {
update_slots ();
});
queue_tasks . on_sleeping_state ([ this ]( bool sleeping ) {
handle_sleeping_state ( sleeping );
});
metrics . init ();
// populate webui settings
{
if ( ! params_base . webui_config_json . empty ()) {
try {
json_webui_settings = json :: parse ( params_base . webui_config_json );
} catch ( const std :: exception & e ) {
SRV_ERR ( "%s: failed to parse webui config: %s \n " , __func__ , e . what ());
return false ;
}
}
}
2026-01-19 23:28:01 +01:00
// populate chat template params
{
common_chat_templates_ptr chat_templates ;
try {
chat_templates = common_chat_templates_init ( model , params_base . chat_template );
LOG_INF ( "%s: chat template, example_format: '%s' \n " , __func__ ,
common_chat_format_example ( chat_templates . get (), params_base . use_jinja , params_base . default_template_kwargs ). c_str ());
} catch ( const std :: exception & e ) {
SRV_ERR ( "%s: chat template parsing error: %s \n " , __func__ , e . what ());
SRV_ERR ( "%s: please consider disabling jinja via --no-jinja, or use a custom chat template via --chat-template \n " , __func__ );
SRV_ERR ( "%s: for example: --no-jinja --chat-template chatml \n " , __func__ );
return false ;
}
// thinking is enabled if:
// 1. It's not explicitly disabled (reasoning_budget == 0)
// 2. The chat template supports it
const bool enable_thinking = params_base . use_jinja && params_base . reasoning_budget != 0 && common_chat_templates_support_enable_thinking ( chat_templates . get ());
SRV_INF ( "%s: chat template, thinking = %d \n " , __func__ , enable_thinking );
chat_params = {
/* use_jinja */ params_base . use_jinja ,
/* prefill_assistant */ params_base . prefill_assistant ,
/* reasoning_format */ params_base . reasoning_format ,
/* chat_template_kwargs */ params_base . default_template_kwargs ,
/* tmpls */ std :: move ( chat_templates ),
/* allow_image */ mctx ? mtmd_support_vision ( mctx ) : false ,
/* allow_audio */ mctx ? mtmd_support_audio ( mctx ) : false ,
/* enable_thinking */ enable_thinking ,
/* media_path */ params_base . media_path ,
};
}
2025-12-21 02:24:42 +01:00
return true ;
2025-11-29 22:04:44 +01:00
}
2026-01-15 17:10:28 +01:00
server_slot * get_slot_by_id ( int id_slot ) {
2026-01-30 19:46:10 +02:00
// note: allow id_slot to be out of bounds (wrap around)
id_slot = id_slot % slots . size ();
2025-11-29 22:04:44 +01:00
for ( server_slot & slot : slots ) {
2026-01-15 17:10:28 +01:00
if ( slot . id == id_slot ) {
2025-11-29 22:04:44 +01:00
return & slot ;
}
}
return nullptr ;
}
server_slot * get_available_slot ( const server_task & task ) {
server_slot * ret = nullptr ;
bool update_cache = false ;
// find the slot that has at least n% prompt similarity
if ( ret == nullptr && slot_prompt_similarity != 0.0f ) {
float sim_best = 0 ;
for ( server_slot & slot : slots ) {
// skip the slot if it is not available
if ( slot . is_processing ()) {
continue ;
}
const auto & tokens = slot . prompt . tokens ;
// skip the slot if it does not contains cached tokens
if ( tokens . empty ()) {
continue ;
}
// fraction of the Longest Common Prefix length with respect to the input prompt length
const float sim_cur = float ( tokens . get_common_prefix ( task . tokens )) / task . tokens . size ();
// select the current slot if the criteria match
if ( sim_cur > sim_best && sim_cur > slot_prompt_similarity ) {
sim_best = sim_cur ;
ret = & slot ;
}
}
if ( ret != nullptr ) {
const float f_keep = ( sim_best * task . tokens . size ()) / ret -> prompt . tokens . size ();
SLT_INF ( * ret , "selected slot by LCP similarity, sim_best = %.3f (> %.3f thold), f_keep = %.3f \n " ,
sim_best , slot_prompt_similarity , f_keep );
// if we are about to lose a large portion of the existing context - save it in the prompt cache
if ( f_keep < 0.5f ) {
update_cache = true ;
}
}
}
// find the slot that has been least recently used
if ( ret == nullptr ) {
int64_t t_last = - 1 ;
for ( server_slot & slot : slots ) {
// skip the slot if it is not available
if ( slot . is_processing ()) {
continue ;
}
// select the current slot if the criteria match
if ( ! ret || slot . t_last_used <= t_last ) {
t_last = slot . t_last_used ;
ret = & slot ;
}
}
if ( ret != nullptr ) {
SLT_INF ( * ret , "selected slot by LRU, t_last = %" PRId64 " \n " , t_last );
update_cache = true ;
}
}
if ( ret ) {
const auto & tokens = ret -> prompt . tokens ;
update_cache = update_cache && prompt_cache ;
// cache prompts only for completion tasks
update_cache = update_cache && task . type == SERVER_TASK_TYPE_COMPLETION ;
// don't update the cache if the slot's context is empty
update_cache = update_cache && tokens . size () > 0 ;
if ( update_cache ) {
SRV_WRN ( "%s" , "updating prompt cache \n " );
const int64_t t_start = ggml_time_us ();
ret -> prompt_save ( * prompt_cache );
if ( ! ret -> prompt_load ( * prompt_cache , task . tokens )) {
2026-01-15 16:39:17 +02:00
ret -> prompt_clear ( false );
2025-11-29 22:04:44 +01:00
}
prompt_cache -> update ();
SRV_WRN ( "prompt cache update took %.2f ms \n " , ( ggml_time_us () - t_start ) / 1000.0 );
}
}
return ret ;
}
// return true if at least one slot has been cleared
// TODO: improve logic
// - smarter decision which slot to clear (LRU or longest prompt?)
// - move slot to level 2 cache instead of removing?
// - instead of purging, try to store and resume later?
bool try_clear_idle_slots () {
bool res = false ;
if ( ! params_base . kv_unified ) {
return res ;
}
for ( auto & slot : slots ) {
if ( slot . is_processing ()) {
continue ;
}
if ( slot . prompt . n_tokens () > 0 ) {
SRV_WRN ( "purging slot %d with %zu tokens \n " , slot . id , slot . prompt . tokens . size ());
2026-01-15 16:39:17 +02:00
slot . prompt_clear ( false );
2025-11-29 22:04:44 +01:00
res = true ;
// clear slots one by one
break ;
}
}
return res ;
}
2026-01-28 18:42:42 +01:00
std :: vector < common_adapter_lora_info > construct_lora_list ( const std :: map < int , float > & config ) const {
2025-12-22 14:23:34 +01:00
std :: vector < common_adapter_lora_info > output = params_base . lora_adapters ; // copy
for ( size_t i = 0 ; i < output . size (); ++ i ) {
auto it = config . find ( i );
if ( it != config . end ()) {
output [ i ]. scale = it -> second ;
} else {
output [ i ]. scale = 0.0f ;
}
}
return output ;
}
2025-11-29 22:04:44 +01:00
bool launch_slot_with_task ( server_slot & slot , server_task && task ) {
2025-12-22 14:23:34 +01:00
// process per-request lora adapters
if ( ! task . params . lora . empty ()) {
auto task_loras = construct_lora_list ( task . params . lora );
if ( ! are_lora_equal ( task_loras , slot . lora )) {
// if lora has changed, check to see if the cache should be cleared
if ( lora_should_clear_cache ( slot . lora , task_loras )) {
SLT_INF ( slot , "clearing cache for lora change. %zu loras -> %zu loras \n " , slot . lora . size (), task . params . lora . size ());
slot . prompt . tokens . clear ();
} else {
SLT_INF ( slot , "keeping cache for alora. %zu target loras \n " , task_loras . size ());
}
slot . lora = task_loras ;
2025-11-29 22:04:44 +01:00
}
2025-12-22 14:23:34 +01:00
} else {
slot . lora = params_base . lora_adapters ;
2025-11-29 22:04:44 +01:00
}
// if using alora, make sure it's only a single one requested and active
size_t alora_invocation_start = task . tokens . size ();
if ( lora_all_alora ( slot . lora )) {
const auto & enabled_ids = lora_get_enabled_ids ( slot . lora );
// TODO: This will error out if a user requests two aloras, but only
// provides the activation string for one. We could, instead search
// for all requested alora activation strings and then either keep
// only the last one, or reject if multiple are found.
if ( enabled_ids . size () != 1 ) {
send_error ( task , "Cannot run multiple aLoRAs in a single request" , ERROR_TYPE_INVALID_REQUEST );
return false ;
}
const auto & lora = slot . lora [ enabled_ids [ 0 ]]. ptr ;
// get the pointer and count for the invocation tokens
const uint64_t n_invocation_tokens = llama_adapter_get_alora_n_invocation_tokens ( lora );
const llama_token * invocation_tokens = llama_adapter_get_alora_invocation_tokens ( lora );
// scan backwards through the prompt tokens to find the last
// occurrence of the invocation sequence
int match_idx = static_cast < int > ( n_invocation_tokens ) - 1 ;
for ( int i = task . tokens . size () - 1 ; i >= 0 ; -- i ) {
// the token in this position matches the next token to find in
// the invocation sequence
if ( task . tokens [ i ] == invocation_tokens [ match_idx ]) {
// if it's a full match, we've found the start
if ( match_idx == 0 ) {
alora_invocation_start = i ;
break ;
}
// otherwise, check the next token in the sequence
-- match_idx ;
} else {
// no match in this position, so start looking over again
match_idx = static_cast < int > ( n_invocation_tokens ) - 1 ;
}
}
// if the activation string is not found, disable the alora
if ( alora_invocation_start == task . tokens . size ()) {
SLT_DBG ( slot , "alora %zu requested, but not found. deactivating \n " , enabled_ids [ 0 ]);
slot . lora [ enabled_ids [ 0 ]]. scale = 0.0f ;
} else {
SLT_DBG ( slot , "alora %zu activated starting at %zu \n " , enabled_ids [ 0 ], alora_invocation_start );
slot . alora_invocation_start = alora_invocation_start ;
}
}
if ( ! task . tokens . validate ( ctx )) {
send_error ( task , "Prompt contains invalid tokens" , ERROR_TYPE_INVALID_REQUEST );
return false ;
}
SLT_DBG ( slot , "launching slot : %s \n " , safe_json_to_str ( slot . to_json ()). c_str ());
// initialize samplers
2026-01-15 16:39:17 +02:00
if ( task . need_sampling ()) {
2025-12-14 10:11:13 +02:00
slot . smpl . reset ( common_sampler_init ( model , task . params . sampling ));
2025-11-29 22:04:44 +01:00
if ( slot . smpl == nullptr ) {
// for now, the only error that may happen here is invalid grammar
send_error ( task , "Failed to parse grammar" , ERROR_TYPE_INVALID_REQUEST );
return false ;
}
2026-01-04 21:22:16 +01:00
const bool need_logits = task . params . sampling . n_probs > 0 ;
bool backend_sampling = true ;
backend_sampling &= task . params . sampling . backend_sampling ;
// TODO: speculative decoding requires multiple samples per batch - not supported yet
2026-01-28 18:42:42 +01:00
backend_sampling &= ! ( slot . spec && task . params . speculative . n_max > 0 );
2026-01-04 21:22:16 +01:00
// TODO: getting post/pre sampling logits is not yet supported with backend sampling
backend_sampling &= ! need_logits ;
// TODO: tmp until backend sampling is fully implemented
if ( backend_sampling ) {
llama_set_sampler ( ctx , slot . id , common_sampler_get ( slot . smpl . get ()));
} else {
llama_set_sampler ( ctx , slot . id , nullptr );
}
2025-12-14 10:11:13 +02:00
SLT_INF ( slot , "sampler chain: %s \n " , common_sampler_print ( slot . smpl . get ()). c_str ());
2026-01-15 16:39:17 +02:00
} else {
slot . smpl . reset ();
2025-11-29 22:04:44 +01:00
}
slot . task = std :: make_unique < const server_task > ( std :: move ( task ));
2026-01-15 17:10:28 +01:00
slot . state = slot . task -> is_child ()
2025-12-06 15:54:38 +01:00
? SLOT_STATE_WAIT_OTHER // wait for the parent to process prompt
: SLOT_STATE_STARTED ;
2025-11-29 22:04:44 +01:00
2026-01-15 17:10:28 +01:00
SLT_INF ( slot , "processing task, is_child = %d \n " , slot . task -> is_child ());
2025-11-29 22:04:44 +01:00
return true ;
}
bool process_token ( completion_token_output & result , server_slot & slot ) {
// remember which tokens were sampled - used for repetition penalties during sampling
const std :: string token_str = result . text_to_send ;
slot . sampled = result . tok ;
slot . generated_text += token_str ;
if ( slot . task -> params . return_tokens ) {
slot . generated_tokens . push_back ( result . tok );
}
slot . has_next_token = true ;
// check if there is incomplete UTF-8 character at the end
bool incomplete = validate_utf8 ( slot . generated_text ) < slot . generated_text . size ();
// search stop word and delete it
if ( ! incomplete ) {
size_t pos = std :: min ( slot . n_sent_text , slot . generated_text . size ());
const std :: string str_test = slot . generated_text . substr ( pos );
bool send_text = true ;
size_t stop_pos = slot . find_stopping_strings ( str_test , token_str . size (), true );
if ( stop_pos != std :: string :: npos ) {
slot . generated_text . erase (
slot . generated_text . begin () + pos + stop_pos ,
slot . generated_text . end ());
pos = std :: min ( slot . n_sent_text , slot . generated_text . size ());
} else if ( slot . has_next_token && ! llama_vocab_is_eog ( vocab , result . tok ) ) {
stop_pos = slot . find_stopping_strings ( str_test , token_str . size (), false );
send_text = stop_pos == std :: string :: npos ;
}
// check if there is any token to predict
if ( send_text ) {
// no send the stop word in the response
result . text_to_send = slot . generated_text . substr ( pos , std :: string :: npos );
slot . n_sent_text += result . text_to_send . size ();
// add the token to slot queue and cache
} else {
result . text_to_send = "" ;
}
slot . add_token ( result );
if ( slot . task -> params . stream ) {
send_partial_response ( slot , result , false );
}
}
if ( incomplete ) {
slot . has_next_token = true ;
}
// if context shifting is disabled, make sure that we don't run out of context
if ( ! params_base . ctx_shift && slot . prompt . n_tokens () + 1 >= slot . n_ctx ) {
slot . truncated = true ;
slot . stop = STOP_TYPE_LIMIT ;
slot . has_next_token = false ;
SLT_DBG ( slot , "stopped due to running out of context capacity, prompt.n_tokens() = %d, task.n_tokens = %d, n_decoded = %d, n_ctx = %d \n " ,
slot . prompt . n_tokens (), slot . task -> n_tokens (), slot . n_decoded , slot . n_ctx );
}
// check the limits
if ( slot . n_decoded > 0 && slot . has_next_token && ! slot . has_budget ( params_base )) {
slot . stop = STOP_TYPE_LIMIT ;
slot . has_next_token = false ;
SLT_DBG ( slot , "stopped by limit, n_decoded = %d, n_predict = %d \n " , slot . n_decoded , slot . task -> params . n_predict );
}
if ( slot . has_new_line ) {
// require that each new line has a whitespace prefix (i.e. indentation) of at least slot.params.n_indent
if ( slot . task -> params . n_indent > 0 ) {
// check the current indentation
// TODO: improve by not doing it more than once for each new line
if ( slot . last_nl_pos > 0 ) {
size_t pos = slot . last_nl_pos ;
int n_indent = 0 ;
while ( pos < slot . generated_text . size () && ( slot . generated_text [ pos ] == ' ' || slot . generated_text [ pos ] == '\t' )) {
n_indent ++ ;
pos ++ ;
}
if ( pos < slot . generated_text . size () && n_indent < slot . task -> params . n_indent ) {
slot . stop = STOP_TYPE_LIMIT ;
slot . has_next_token = false ;
// cut the last line
slot . generated_text . erase ( pos , std :: string :: npos );
SLT_DBG ( slot , "stopped by indentation limit, n_decoded = %d, n_indent = %d \n " , slot . n_decoded , n_indent );
}
}
// find the next new line
{
const size_t pos = slot . generated_text . find ( '\n' , slot . last_nl_pos );
if ( pos != std :: string :: npos ) {
slot . last_nl_pos = pos + 1 ;
}
}
}
}
// check if there is a new line in the generated text
if ( result . text_to_send . find ( '\n' ) != std :: string :: npos ) {
slot . has_new_line = true ;
// if we have seen a new line, we stop after a certain time limit, but only upon another new line
if ( slot . task -> params . t_max_predict_ms > 0 && ( ggml_time_us () - slot . t_start_generation > 1000.0f * slot . task -> params . t_max_predict_ms )) {
slot . stop = STOP_TYPE_LIMIT ;
slot . has_next_token = false ;
SLT_DBG ( slot , "stopped by time limit, n_decoded = %d, t_max_predict_ms = %d ms \n " , slot . n_decoded , ( int ) slot . task -> params . t_max_predict_ms );
}
}
if ( llama_vocab_is_eog ( vocab , result . tok )) {
slot . stop = STOP_TYPE_EOS ;
slot . has_next_token = false ;
SLT_DBG ( slot , "%s" , "stopped by EOS \n " );
}
SLT_DBG ( slot , "n_decoded = %d, n_remaining = %d, next token: %5d '%s' \n " , slot . n_decoded , slot . n_remaining , result . tok , token_str . c_str ());
return slot . has_next_token ; // continue
}
void populate_token_probs ( const server_slot & slot , completion_token_output & result , bool post_sampling , bool special , int idx ) const {
2026-01-19 19:13:31 +01:00
const size_t n_probs_request = slot . task -> params . sampling . n_probs ;
2025-11-29 22:04:44 +01:00
if ( post_sampling ) {
2025-12-14 10:11:13 +02:00
const auto * cur_p = common_sampler_get_candidates ( slot . smpl . get (), true );
2025-11-29 22:04:44 +01:00
const size_t max_probs = cur_p -> size ;
2026-01-19 19:13:31 +01:00
const size_t n_probs = std :: min ( max_probs , n_probs_request );
2025-11-29 22:04:44 +01:00
// set probability for sampled token
for ( size_t i = 0 ; i < max_probs ; i ++ ) {
if ( cur_p -> data [ i ]. id == result . tok ) {
result . prob = cur_p -> data [ i ]. p ;
break ;
}
}
// set probability for top n_probs tokens
2026-01-19 19:13:31 +01:00
result . probs . reserve ( n_probs );
for ( size_t i = 0 ; i < n_probs ; i ++ ) {
2025-11-29 22:04:44 +01:00
result . probs . push_back ({
cur_p -> data [ i ]. id ,
common_token_to_piece ( ctx , cur_p -> data [ i ]. id , special ),
cur_p -> data [ i ]. p
});
}
} else {
// TODO: optimize this with min-p optimization
std :: vector < llama_token_data > cur = get_token_probabilities ( ctx , idx );
2026-01-19 19:13:31 +01:00
const size_t max_probs = cur . size ();
const size_t n_probs = std :: min ( max_probs , n_probs_request );
2025-11-29 22:04:44 +01:00
// set probability for sampled token
2026-01-19 19:13:31 +01:00
for ( size_t i = 0 ; i < max_probs ; i ++ ) {
2025-11-29 22:04:44 +01:00
// set probability for sampled token
if ( cur [ i ]. id == result . tok ) {
result . prob = cur [ i ]. p ;
break ;
}
}
// set probability for top n_probs tokens
result . probs . reserve ( n_probs );
2026-01-19 19:13:31 +01:00
for ( size_t i = 0 ; i < n_probs ; i ++ ) {
2025-11-29 22:04:44 +01:00
result . probs . push_back ({
cur [ i ]. id ,
common_token_to_piece ( ctx , cur [ i ]. id , special ),
cur [ i ]. p
});
}
}
}
void send_error ( const server_task & task , const std :: string & error , const enum error_type type = ERROR_TYPE_SERVER ) {
send_error ( task . id , error , type );
}
void send_error ( const server_slot & slot , const std :: string & error , const enum error_type type = ERROR_TYPE_SERVER ) {
send_error ( slot . task -> id , error , type , slot . task -> n_tokens (), slot . n_ctx );
}
void send_error ( const int id_task , const std :: string & error , const enum error_type type = ERROR_TYPE_SERVER , const int32_t n_prompt_tokens = 0 , const int32_t n_ctx = 0 ) {
SRV_ERR ( "task id = %d, error: %s \n " , id_task , error . c_str ());
if ( type == ERROR_TYPE_EXCEED_CONTEXT_SIZE ) {
GGML_ASSERT ( n_ctx > 0 && n_prompt_tokens > 0 );
}
auto res = std :: make_unique < server_task_result_error > ();
res -> id = id_task ;
res -> err_type = type ;
res -> err_msg = error ;
res -> n_prompt_tokens = n_prompt_tokens ;
res -> n_ctx = n_ctx ;
queue_results . send ( std :: move ( res ));
}
// if multimodal is enabled, send an error and return false
bool check_no_mtmd ( const int id_task ) {
if ( mctx ) {
send_error ( id_task , "This feature is not supported by multimodal" , ERROR_TYPE_NOT_SUPPORTED );
return false ;
}
return true ;
}
void send_partial_response ( server_slot & slot , const completion_token_output & tkn , bool is_progress ) {
auto res = std :: make_unique < server_task_result_cmpl_partial > ();
res -> id = slot . task -> id ;
res -> index = slot . task -> index ;
if ( is_progress ) {
res -> is_progress = true ;
res -> progress . total = slot . task -> n_tokens ();
res -> progress . cache = slot . n_prompt_tokens_cache ;
res -> progress . processed = slot . prompt . tokens . size ();
res -> progress . time_ms = ( ggml_time_us () - slot . t_start_process_prompt ) / 1000 ;
} else {
res -> content = tkn . text_to_send ;
res -> tokens = { tkn . tok };
}
res -> n_decoded = slot . n_decoded ;
res -> n_prompt_tokens = slot . task -> n_tokens ();
res -> post_sampling_probs = slot . task -> params . post_sampling_probs ;
res -> verbose = slot . task -> params . verbose ;
res -> res_type = slot . task -> params . res_type ;
res -> oaicompat_model = slot . task -> params . oaicompat_model ;
res -> oaicompat_cmpl_id = slot . task -> params . oaicompat_cmpl_id ;
// populate res.probs_output
if ( slot . task -> params . sampling . n_probs > 0 ) {
res -> prob_output = tkn ; // copy the token probs
}
// populate timings if this is final response or timings_per_token is enabled
if ( slot . stop != STOP_TYPE_NONE || slot . task -> params . timings_per_token ) {
res -> timings = slot . get_timings ();
}
queue_results . send ( std :: move ( res ));
}
void send_final_response ( server_slot & slot ) {
auto res = std :: make_unique < server_task_result_cmpl_final > ();
res -> id = slot . task -> id ;
res -> id_slot = slot . id ;
2026-02-25 15:14:27 +02:00
res -> index = slot . task -> index ;
2026-02-18 18:53:37 +01:00
// keep copy of last generated text for debugging purposes
if ( slots_debug ) {
slot . debug_generated_text = slot . generated_text ;
}
2025-12-04 15:46:08 +01:00
// in stream mode, content and tokens are already in last partial chunk
if ( slot . task -> params . stream ) {
res -> content = "" ;
res -> tokens = llama_tokens {};
} else {
res -> content = std :: move ( slot . generated_text );
res -> tokens = std :: move ( slot . generated_tokens );
}
2025-11-29 22:04:44 +01:00
res -> timings = slot . get_timings ();
res -> prompt = slot . task -> tokens . detokenize ( ctx , true );
res -> response_fields = std :: move ( slot . task -> params . response_fields );
res -> truncated = slot . truncated ;
res -> n_decoded = slot . n_decoded ;
res -> n_prompt_tokens = slot . task -> n_tokens ();
res -> n_tokens_cached = slot . prompt . n_tokens ();
res -> has_new_line = slot . has_new_line ;
res -> stopping_word = slot . stopping_word ;
res -> stop = slot . stop ;
res -> post_sampling_probs = slot . task -> params . post_sampling_probs ;
res -> verbose = slot . task -> params . verbose ;
res -> stream = slot . task -> params . stream ;
res -> include_usage = slot . task -> params . include_usage ;
res -> res_type = slot . task -> params . res_type ;
res -> oaicompat_model = slot . task -> params . oaicompat_model ;
res -> oaicompat_cmpl_id = slot . task -> params . oaicompat_cmpl_id ;
// populate res.probs_output
if ( slot . task -> params . sampling . n_probs > 0 ) {
if ( ! slot . task -> params . stream && slot . stop == STOP_TYPE_WORD ) {
const llama_tokens stop_word_toks = common_tokenize ( ctx , slot . stopping_word , false );
size_t safe_offset = std :: min ( slot . generated_token_probs . size (), stop_word_toks . size ());
res -> probs_output = std :: vector < completion_token_output > (
slot . generated_token_probs . begin (),
slot . generated_token_probs . end () - safe_offset );
} else {
res -> probs_output = std :: vector < completion_token_output > (
slot . generated_token_probs . begin (),
slot . generated_token_probs . end ());
}
}
res -> generation_params = slot . task -> params ; // copy the parameters
queue_results . send ( std :: move ( res ));
}
void send_embedding ( const server_slot & slot , const llama_batch & batch ) {
auto res = std :: make_unique < server_task_result_embd > ();
res -> id = slot . task -> id ;
res -> index = slot . task -> index ;
res -> n_tokens = slot . task -> n_tokens ();
res -> res_type = slot . task -> params . res_type ;
2026-01-05 19:52:56 +01:00
const int n_embd_out = llama_model_n_embd_out ( model );
2025-11-29 22:04:44 +01:00
2026-01-05 19:52:56 +01:00
std :: vector < float > embd_res ( n_embd_out , 0.0f );
2025-11-29 22:04:44 +01:00
for ( int i = 0 ; i < batch . n_tokens ; ++ i ) {
if ( ! batch . logits [ i ] || batch . seq_id [ i ][ 0 ] != slot . id ) {
continue ;
}
const float * embd = nullptr ;
if ( llama_pooling_type ( slot . ctx ) == LLAMA_POOLING_TYPE_NONE ) {
embd = llama_get_embeddings_ith ( ctx , i );
} else {
embd = llama_get_embeddings_seq ( ctx , batch . seq_id [ i ][ 0 ]);
}
if ( embd == nullptr ) {
SLT_ERR ( slot , "failed to get embeddings, token = %d, seq_id = %d \n " , batch . token [ i ], batch . seq_id [ i ][ 0 ]);
2026-01-05 19:52:56 +01:00
res -> embedding . push_back ( std :: vector < float > ( n_embd_out , 0.0f ));
2025-11-29 22:04:44 +01:00
continue ;
}
// normalize only when there is pooling
if ( llama_pooling_type ( slot . ctx ) != LLAMA_POOLING_TYPE_NONE ) {
2026-01-05 19:52:56 +01:00
common_embd_normalize ( embd , embd_res . data (), n_embd_out , slot . task -> params . embd_normalize );
2025-11-29 22:04:44 +01:00
res -> embedding . push_back ( embd_res );
break ;
}
2026-01-05 19:52:56 +01:00
res -> embedding . emplace_back ( embd , embd + n_embd_out );
2025-11-29 22:04:44 +01:00
}
SLT_DBG ( slot , "%s" , "sending embeddings \n " );
queue_results . send ( std :: move ( res ));
}
void send_rerank ( const server_slot & slot , const llama_batch & batch ) {
auto res = std :: make_unique < server_task_result_rerank > ();
res -> id = slot . task -> id ;
res -> index = slot . task -> index ;
res -> n_tokens = slot . task -> n_tokens ();
for ( int i = 0 ; i < batch . n_tokens ; ++ i ) {
if ( ! batch . logits [ i ] || batch . seq_id [ i ][ 0 ] != slot . id ) {
continue ;
}
const float * embd = llama_get_embeddings_seq ( ctx , batch . seq_id [ i ][ 0 ]);
if ( embd == NULL ) {
embd = llama_get_embeddings_ith ( ctx , i );
}
if ( embd == NULL ) {
SLT_ERR ( slot , "failed to get embeddings, token = %d, seq_id = %d \n " , batch . token [ i ], batch . seq_id [ i ][ 0 ]);
res -> score = - 1e6 ;
continue ;
}
res -> score = embd [ 0 ];
}
SLT_DBG ( slot , "sending rerank result, res.score = %f \n " , res -> score );
queue_results . send ( std :: move ( res ));
}
//
// Functions to process the task
//
2025-12-10 15:28:59 +01:00
// tokenize the input if it's set by CLI, return false on error
bool tokenize_cli_input ( server_task & task ) {
try {
2026-01-19 23:28:01 +01:00
auto & prompt = task . cli_prompt ;
2025-12-10 15:28:59 +01:00
if ( mctx != nullptr ) {
task . tokens = process_mtmd_prompt ( mctx , prompt , task . cli_files );
} else {
task . tokens = std :: move ( tokenize_input_prompts ( vocab , mctx , prompt , true , true )[ 0 ]);
}
2026-01-19 23:28:01 +01:00
task . cli_prompt . clear ();
2025-12-10 15:28:59 +01:00
task . cli_files . clear ();
} catch ( const std :: exception & e ) {
send_error ( task , std :: string ( "Failed to format input: " ) + e . what (), ERROR_TYPE_INVALID_REQUEST );
return false ;
}
return true ;
}
2026-01-15 17:10:28 +01:00
std :: vector < server_slot *> get_free_slots ( size_t n_slots_needed , int exclude_id_slot ) {
std :: vector < server_slot *> free_slots ;
for ( auto & slot : slots ) {
if ( ! slot . is_processing () && slot . id != exclude_id_slot ) {
free_slots . push_back ( & slot );
}
if ( free_slots . size () >= n_slots_needed ) {
break ;
}
}
return free_slots ;
}
// launch multiple slots for parent + child tasks
bool launch_slots_with_parent_task ( server_slot & parent_slot , std :: vector < server_slot *> & child_slots , server_task && parent_task ) {
GGML_ASSERT ( ! parent_slot . is_processing ());
GGML_ASSERT ( parent_task . is_parent ());
GGML_ASSERT ( child_slots . size () == parent_task . child_tasks . size ());
int id_parent = parent_task . id ;
SRV_INF ( "launching slots for parent task id_task = %d with %zu child tasks \n " , id_parent , parent_task . child_tasks . size ());
// to be called in case of failure to release all launched slots
auto release_slots = [ this , id_parent ]() {
for ( auto & slot : slots ) {
if ( slot . is_processing () && (
slot . task -> id == id_parent ||
slot . task -> id_parent == id_parent
)) {
slot . release ();
}
}
};
// launch all child tasks first
size_t idx = 0 ;
GGML_ASSERT ( child_slots . size () == parent_task . child_tasks . size ());
for ( auto * slot : child_slots ) {
int id_child = parent_task . child_tasks [ idx ]. id ;
if ( ! launch_slot_with_task ( * slot , std :: move ( parent_task . child_tasks [ idx ]))) {
SRV_ERR ( "failed to launch slot with child task, id_task = %d \n " , id_child );
release_slots ();
return false ;
}
idx ++ ;
}
// finally, launch the parent task
if ( ! launch_slot_with_task ( parent_slot , std :: move ( parent_task ))) {
SRV_ERR ( "failed to launch slot with task, id_task = %d \n " , id_parent );
release_slots ();
return false ;
}
return true ;
}
2025-11-29 22:04:44 +01:00
void process_single_task ( server_task && task ) {
switch ( task . type ) {
case SERVER_TASK_TYPE_COMPLETION :
case SERVER_TASK_TYPE_INFILL :
case SERVER_TASK_TYPE_EMBEDDING :
case SERVER_TASK_TYPE_RERANK :
{
2026-01-15 17:10:28 +01:00
// special case: if input is provided via CLI, tokenize it first
// otherwise, no need to tokenize as it's already done inside the HTTP thread
2026-01-19 23:28:01 +01:00
if ( task . cli ) {
2026-01-15 17:10:28 +01:00
if ( ! tokenize_cli_input ( task )) {
break ;
}
2025-12-10 15:28:59 +01:00
}
2025-11-29 22:04:44 +01:00
const int id_slot = task . id_slot ;
2026-01-15 17:10:28 +01:00
const int id_task = task . id ;
2025-11-29 22:04:44 +01:00
2026-01-15 17:10:28 +01:00
server_slot * slot = id_slot != - 1
? get_slot_by_id ( id_slot )
: get_available_slot ( task );
//
// slot scheduling logic
//
2025-11-29 22:04:44 +01:00
if ( slot == nullptr ) {
// if no slot is available, we defer this task for processing later
2026-01-15 17:10:28 +01:00
SRV_DBG ( "no slot is available, defer task, id_task = %d \n " , id_task );
2025-11-29 22:04:44 +01:00
queue_tasks . defer ( std :: move ( task ));
break ;
}
if ( slot -> is_processing ()) {
// if requested slot is unavailable, we defer this task for processing later
2026-01-15 17:10:28 +01:00
SRV_DBG ( "requested slot is unavailable, defer task, id_task = %d \n " , id_task );
2025-11-29 22:04:44 +01:00
queue_tasks . defer ( std :: move ( task ));
break ;
}
2026-01-15 17:10:28 +01:00
if ( task . is_parent ()) {
// try getting free slots for all child tasks
size_t n_child_tasks = task . child_tasks . size ();
std :: vector < server_slot *> child_slots = get_free_slots ( n_child_tasks , slot -> id );
if ( child_slots . size () < n_child_tasks ) {
SRV_DBG ( "not enough free slots for child tasks, n_free = %zu, n_children = %zu, defer task, id_task = %d \n " , child_slots . size (), n_child_tasks , id_task );
queue_tasks . defer ( std :: move ( task ));
break ;
}
if ( ! launch_slots_with_parent_task ( * slot , child_slots , std :: move ( task ))) {
SRV_ERR ( "failed to launch slot with parent task, id_task = %d \n " , id_task );
break ; // drop the task
}
} else if ( ! launch_slot_with_task ( * slot , std :: move ( task ))) {
SRV_ERR ( "failed to launch slot with task, id_task = %d \n " , id_task );
break ; // drop the task
2025-11-29 22:04:44 +01:00
}
} break ;
case SERVER_TASK_TYPE_CANCEL :
{
// release slot linked with the task id
for ( auto & slot : slots ) {
if ( slot . task && slot . task -> id == task . id_target ) {
slot . release ();
break ;
}
}
} break ;
case SERVER_TASK_TYPE_NEXT_RESPONSE :
{
// do nothing
} break ;
case SERVER_TASK_TYPE_METRICS :
{
json slots_data = json :: array ();
int n_idle_slots = 0 ;
int n_processing_slots = 0 ;
for ( server_slot & slot : slots ) {
json slot_data = slot . to_json ( slots_debug == 0 );
if ( slot . is_processing ()) {
n_processing_slots ++ ;
} else {
n_idle_slots ++ ;
}
slots_data . push_back ( slot_data );
}
SRV_DBG ( "n_idle_slots = %d, n_processing_slots = %d \n " , n_idle_slots , n_processing_slots );
auto res = std :: make_unique < server_task_result_metrics > ();
res -> id = task . id ;
res -> slots_data = std :: move ( slots_data );
res -> n_idle_slots = n_idle_slots ;
res -> n_processing_slots = n_processing_slots ;
res -> n_tasks_deferred = queue_tasks . queue_tasks_deferred_size ();
res -> t_start = metrics . t_start ;
res -> n_prompt_tokens_processed_total = metrics . n_prompt_tokens_processed_total ;
res -> t_prompt_processing_total = metrics . t_prompt_processing_total ;
res -> n_tokens_predicted_total = metrics . n_tokens_predicted_total ;
res -> t_tokens_generation_total = metrics . t_tokens_generation_total ;
res -> n_tokens_max = metrics . n_tokens_max ;
res -> n_prompt_tokens_processed = metrics . n_prompt_tokens_processed ;
res -> t_prompt_processing = metrics . t_prompt_processing ;
res -> n_tokens_predicted = metrics . n_tokens_predicted ;
res -> t_tokens_generation = metrics . t_tokens_generation ;
res -> n_decode_total = metrics . n_decode_total ;
res -> n_busy_slots_total = metrics . n_busy_slots_total ;
if ( task . metrics_reset_bucket ) {
metrics . reset_bucket ();
}
queue_results . send ( std :: move ( res ));
} break ;
case SERVER_TASK_TYPE_SLOT_SAVE :
{
if ( ! check_no_mtmd ( task . id )) {
break ;
}
2026-01-30 19:46:10 +02:00
const int id_slot = task . slot_action . id_slot ;
2025-11-29 22:04:44 +01:00
server_slot * slot = get_slot_by_id ( id_slot );
if ( slot == nullptr ) {
send_error ( task , "Invalid slot ID" , ERROR_TYPE_INVALID_REQUEST );
break ;
}
if ( slot -> is_processing ()) {
// if requested slot is unavailable, we defer this task for processing later
SRV_DBG ( "requested slot is unavailable, defer task, id_task = %d \n " , task . id );
queue_tasks . defer ( std :: move ( task ));
break ;
}
const size_t token_count = slot -> prompt . tokens . size ();
const int64_t t_start = ggml_time_us ();
std :: string filename = task . slot_action . filename ;
std :: string filepath = task . slot_action . filepath ;
const llama_tokens & tokens = slot -> prompt . tokens . get_text_tokens ();
const size_t nwrite = llama_state_seq_save_file ( ctx , filepath . c_str (), slot -> id , tokens . data (), token_count );
const int64_t t_end = ggml_time_us ();
const double t_save_ms = ( t_end - t_start ) / 1000.0 ;
auto res = std :: make_unique < server_task_result_slot_save_load > ();
res -> id = task . id ;
res -> id_slot = id_slot ;
res -> filename = filename ;
res -> is_save = true ;
res -> n_tokens = token_count ;
res -> n_bytes = nwrite ;
res -> t_ms = t_save_ms ;
queue_results . send ( std :: move ( res ));
} break ;
case SERVER_TASK_TYPE_SLOT_RESTORE :
{
if ( ! check_no_mtmd ( task . id )) break ;
2026-01-30 19:46:10 +02:00
const int id_slot = task . slot_action . id_slot ;
2025-11-29 22:04:44 +01:00
server_slot * slot = get_slot_by_id ( id_slot );
if ( slot == nullptr ) {
send_error ( task , "Invalid slot ID" , ERROR_TYPE_INVALID_REQUEST );
break ;
}
if ( slot -> is_processing ()) {
// if requested slot is unavailable, we defer this task for processing later
SRV_DBG ( "requested slot is unavailable, defer task, id_task = %d \n " , task . id );
queue_tasks . defer ( std :: move ( task ));
break ;
}
const int64_t t_start = ggml_time_us ();
std :: string filename = task . slot_action . filename ;
std :: string filepath = task . slot_action . filepath ;
llama_tokens tokens ;
tokens . resize ( slot -> n_ctx );
size_t token_count = 0 ;
size_t nread = llama_state_seq_load_file ( ctx , filepath . c_str (), slot -> id , tokens . data (), tokens . size (), & token_count );
if ( nread == 0 ) {
slot -> prompt . tokens . clear (); // KV may already been invalidated?
send_error ( task , "Unable to restore slot, no available space in KV cache or invalid slot save file" , ERROR_TYPE_INVALID_REQUEST );
break ;
}
tokens . resize ( token_count );
slot -> prompt . tokens . clear ();
slot -> prompt . tokens . insert ( tokens );
const int64_t t_end = ggml_time_us ();
const double t_restore_ms = ( t_end - t_start ) / 1000.0 ;
auto res = std :: make_unique < server_task_result_slot_save_load > ();
res -> id = task . id ;
res -> id_slot = id_slot ;
res -> filename = filename ;
res -> is_save = false ;
res -> n_tokens = token_count ;
res -> n_bytes = nread ;
res -> t_ms = t_restore_ms ;
queue_results . send ( std :: move ( res ));
} break ;
case SERVER_TASK_TYPE_SLOT_ERASE :
{
if ( ! check_no_mtmd ( task . id )) {
break ;
}
2026-01-30 19:46:10 +02:00
const int id_slot = task . slot_action . id_slot ;
2025-11-29 22:04:44 +01:00
server_slot * slot = get_slot_by_id ( id_slot );
if ( slot == nullptr ) {
send_error ( task , "Invalid slot ID" , ERROR_TYPE_INVALID_REQUEST );
break ;
}
if ( slot -> is_processing ()) {
// if requested slot is unavailable, we defer this task for processing later
SRV_DBG ( "requested slot is unavailable, defer task, id_task = %d \n " , task . id );
queue_tasks . defer ( std :: move ( task ));
break ;
}
// Erase token cache
const size_t n_erased = slot -> prompt . tokens . size ();
2026-01-15 16:39:17 +02:00
slot -> prompt_clear ( false );
2025-11-29 22:04:44 +01:00
auto res = std :: make_unique < server_task_result_slot_erase > ();
res -> id = task . id ;
res -> id_slot = id_slot ;
res -> n_erased = n_erased ;
queue_results . send ( std :: move ( res ));
} break ;
2025-12-22 14:23:34 +01:00
case SERVER_TASK_TYPE_GET_LORA :
{
// TODO @ngxson : make lora_adapters a dedicated member of server_context
auto & loras = params_base . lora_adapters ;
auto res = std :: make_unique < server_task_result_get_lora > ();
res -> id = task . id ;
for ( size_t i = 0 ; i < loras . size (); ++ i ) {
auto & lora = loras [ i ];
std :: string alora_invocation_string = "" ;
const uint64_t n_alora_tokens = llama_adapter_get_alora_n_invocation_tokens ( lora . ptr );
llama_tokens alora_invocation_tokens ;
if ( n_alora_tokens ) {
const llama_token * alora_tokens = llama_adapter_get_alora_invocation_tokens ( lora . ptr );
for ( uint64_t j = 0 ; j < n_alora_tokens ; ++ j ) {
alora_invocation_string += common_token_to_piece ( vocab , alora_tokens [ j ]);
alora_invocation_tokens . push_back ( alora_tokens [ j ]);
}
}
res -> loras . push_back ( server_task_result_get_lora :: lora {
lora ,
alora_invocation_string ,
alora_invocation_tokens ,
});
}
queue_results . send ( std :: move ( res ));
} break ;
2025-11-29 22:04:44 +01:00
case SERVER_TASK_TYPE_SET_LORA :
{
2025-12-22 14:23:34 +01:00
auto new_loras = construct_lora_list ( task . set_lora );
// logging
for ( size_t i = 0 ; i < new_loras . size (); ++ i ) {
SRV_INF ( "set lora adapter idx=%zu scale=%f \n " , i , new_loras [ i ]. scale );
}
// TODO @ngxson : make lora_adapters a dedicated member of server_context
params_base . lora_adapters = new_loras ;
2025-11-29 22:04:44 +01:00
auto res = std :: make_unique < server_task_result_apply_lora > ();
res -> id = task . id ;
queue_results . send ( std :: move ( res ));
} break ;
}
}
void update_slots () {
// check if all slots are idle
{
bool all_idle = true ;
for ( auto & slot : slots ) {
if ( slot . is_processing ()) {
all_idle = false ;
break ;
}
}
if ( all_idle ) {
SRV_INF ( "%s" , "all slots are idle \n " );
return ;
}
}
{
SRV_DBG ( "%s" , "posting NEXT_RESPONSE \n " );
server_task task ( SERVER_TASK_TYPE_NEXT_RESPONSE );
task . id = queue_tasks . get_new_id ();
queue_tasks . post ( std :: move ( task ));
}
// apply context-shift if needed
// TODO: simplify and improve
for ( server_slot & slot : slots ) {
if ( slot . state == SLOT_STATE_GENERATING && slot . prompt . n_tokens () + 1 >= slot . n_ctx ) {
if ( ! params_base . ctx_shift ) {
// this check is redundant (for good)
// we should never get here, because generation should already stopped in process_token()
send_error ( slot , "context shift is disabled" , ERROR_TYPE_SERVER );
slot . release ();
continue ;
}
if ( mctx ) {
// we should never reach this because params_base.ctx_shift is automatically disabled if mmproj is loaded
// we don't support ctx_shift because an image chunk may contains multiple tokens
GGML_ABORT ( "not supported by multimodal" );
}
2026-01-15 17:10:28 +01:00
if ( slot . task -> is_parent () || slot . task -> is_child ()) {
2025-12-06 15:54:38 +01:00
send_error ( slot , "context shift cannot be used for shared prompt" , ERROR_TYPE_SERVER );
slot . release ();
continue ;
}
2025-11-29 22:04:44 +01:00
// Shift context
int n_keep = slot . task -> params . n_keep < 0 ? slot . task -> n_tokens () : slot . task -> params . n_keep ;
if ( add_bos_token ) {
n_keep += 1 ;
}
n_keep = std :: min ( slot . n_ctx - 4 , n_keep );
const int n_left = slot . prompt . n_tokens () - n_keep ;
const int n_discard = slot . task -> params . n_discard ? slot . task -> params . n_discard : ( n_left / 2 );
SLT_WRN ( slot , "slot context shift, n_keep = %d, n_left = %d, n_discard = %d \n " , n_keep , n_left , n_discard );
llama_memory_seq_rm ( llama_get_memory ( ctx ), slot . id , n_keep , n_keep + n_discard );
llama_memory_seq_add ( llama_get_memory ( ctx ), slot . id , n_keep + n_discard , slot . prompt . n_tokens (), - n_discard );
// add generated tokens to cache
// ref: https://github.com/ggml-org/llama.cpp/pull/16818#discussion_r2473269481
{
GGML_ASSERT ( ! slot . prompt . tokens . has_mtmd );
llama_tokens new_tokens = slot . prompt . tokens . get_text_tokens (); // copy
for ( size_t i = n_keep + n_discard ; i < new_tokens . size (); i ++ ) {
new_tokens [ i - n_discard ] = new_tokens [ i ];
}
new_tokens . resize ( slot . prompt . tokens . size () - n_discard );
slot . prompt . tokens . clear ();
slot . prompt . tokens . insert ( new_tokens );
}
slot . truncated = true ;
}
}
// start populating the batch for this iteration
common_batch_clear ( batch );
// track if given slot can be batched with slots already in the batch
server_slot * slot_batched = nullptr ;
auto accept_special_token = [ & ]( server_slot & slot , llama_token token ) {
return params_base . special ||
slot . task -> params . sampling . preserved_tokens . find ( token ) != slot . task -> params . sampling . preserved_tokens . end ();
};
// first, add sampled tokens from any ongoing sequences
for ( auto & slot : slots ) {
if ( slot . state != SLOT_STATE_GENERATING ) {
continue ;
}
// check if we can batch this slot with the previous one
if ( ! slot_batched ) {
slot_batched = & slot ;
} else if ( ! slot_batched -> can_batch_with ( slot )) {
continue ;
}
2025-12-08 14:35:28 +01:00
// generate draft tokens in speculative decoding mode
// TODO: rework to have a single draft llama_context shared across all slots [TAG_SERVER_SPEC_REWORK]
// perform the speculative drafting for all sequences at the same time in a single batch
2026-01-28 18:42:42 +01:00
const int n_draft_max = slot . get_n_draft_max ();
2025-12-08 14:35:28 +01:00
if ( n_draft_max > 0 ) {
if ( mctx ) {
// we should never reach this, as speculative is automatically disabled if mmproj is loaded
GGML_ABORT ( "not supported by multimodal" );
}
2025-11-29 22:04:44 +01:00
2025-12-08 14:35:28 +01:00
const llama_tokens & cached_text_tokens = slot . prompt . tokens . get_text_tokens ();
2026-01-28 18:42:42 +01:00
const auto & params_spec = slot . task -> params . speculative ;
llama_tokens draft = common_speculative_draft ( slot . spec , params_spec , cached_text_tokens , slot . sampled );
if ( draft . size () > ( size_t ) n_draft_max ) {
SLT_WRN ( slot , "draft size %d exceeds max %d, truncating \n " , ( int ) draft . size (), n_draft_max );
draft . resize ( n_draft_max );
}
2025-11-29 22:04:44 +01:00
2025-12-08 14:35:28 +01:00
// add the sampled token to the batch
slot . i_batch_dft . push_back ( batch . n_tokens );
common_batch_add ( batch , slot . sampled , slot . prompt . tokens . pos_next (), { slot . id }, true );
slot . prompt . tokens . push_back ( slot . sampled );
2025-11-29 22:04:44 +01:00
2025-12-08 14:35:28 +01:00
if ( slot . task -> params . speculative . n_min > ( int ) draft . size ()) {
SLT_DBG ( slot , "ignoring small draft: %d < %d \n " , ( int ) draft . size (), slot . task -> params . speculative . n_min );
// fallback to normal decoding
slot . i_batch = slot . i_batch_dft [ 0 ];
slot . drafted . clear ();
slot . i_batch_dft . clear ();
} else {
// keep track of total number of drafted tokens tested
slot . n_draft_total += draft . size ();
// add all drafted tokens to the batch
for ( size_t i = 0 ; i < draft . size (); i ++ ) {
slot . i_batch_dft . push_back ( batch . n_tokens );
common_batch_add ( batch , draft [ i ], slot . prompt . tokens . pos_next (), { slot . id }, true );
slot . prompt . tokens . push_back ( draft [ i ]);
}
slot . drafted = std :: move ( draft );
}
} else {
// no speculative decoding
slot . i_batch = batch . n_tokens ;
common_batch_add ( batch , slot . sampled , slot . prompt . tokens . pos_next (), { slot . id }, true );
slot . prompt . tokens . push_back ( slot . sampled );
SLT_DBG ( slot , "slot decode token, n_ctx = %d, n_tokens = %d, truncated = %d \n " ,
slot . n_ctx , slot . prompt . n_tokens (), slot . truncated );
}
2025-11-29 22:04:44 +01:00
}
// process in chunks of params.n_batch
int32_t n_batch = llama_n_batch ( ctx );
int32_t n_ubatch = llama_n_ubatch ( ctx );
float alora_scale = - 1.0f ;
size_t alora_disabled_id = 0 ;
// next, batch any pending prompts without exceeding n_batch
if ( params_base . cont_batching || batch . n_tokens == 0 ) {
for ( auto & slot : slots ) {
if ( ! slot . is_processing ()) {
continue ;
}
// check if we can batch this slot with the previous one
if ( slot_batched && ! slot_batched -> can_batch_with ( slot )) {
continue ;
}
2026-01-10 00:00:41 +01:00
// check if this is a child slot
if ( slot . state == SLOT_STATE_WAIT_OTHER ) {
SLT_DBG ( slot , "%s" , "waiting for parent slot to complete \n " );
continue ;
}
2025-11-29 22:04:44 +01:00
// this slot still has a prompt to be processed
if ( slot . state == SLOT_STATE_PROCESSING_PROMPT || slot . state == SLOT_STATE_STARTED ) {
const auto & input_tokens = slot . task -> tokens ;
// TODO: maybe move branch to outside of this loop in the future
if ( slot . state == SLOT_STATE_STARTED ) {
slot . t_start_process_prompt = ggml_time_us ();
slot . t_start_generation = 0 ;
slot . state = SLOT_STATE_PROCESSING_PROMPT ;
SLT_INF ( slot , "new prompt, n_ctx_slot = %d, n_keep = %d, task.n_tokens = %d \n " ,
slot . n_ctx , slot . task -> params . n_keep , slot . task -> n_tokens ());
// print prompt tokens (for debugging)
/*if (1) {
// first 16 tokens (avoid flooding logs)
for (int i = 0; i < std::min<int>(16, input_tokens.size()); i++) {
SLT_DBG(slot, "prompt token %3d: %6d '%s'\n", i, input_tokens[i], common_token_to_piece(ctx, input_tokens[i]).c_str());
}
} else {
// all
for (int i = 0; i < (int) input_tokens.size(); i++) {
SLT_DBG(slot, "prompt token %3d: %6d '%s'\n", i, input_tokens[i], common_token_to_piece(ctx, input_tokens[i]).c_str());
}
}*/
// keep track how many tokens we can reuse from the previous state
int n_past = 0 ;
// empty prompt passed -> release the slot and send empty response
if ( input_tokens . empty ()) {
SLT_WRN ( slot , "%s" , "empty prompt - releasing slot \n " );
slot . print_timings ();
send_final_response ( slot );
slot . release ();
continue ;
}
// TODO: support memory-less logits computation
2026-01-15 16:39:17 +02:00
if ( slot . task -> need_logits () && ! llama_get_memory ( ctx )) {
2025-11-29 22:04:44 +01:00
send_error ( slot , "the current context does not logits computation. skipping" , ERROR_TYPE_SERVER );
slot . release ();
continue ;
}
if ( ! slot . can_split ()) {
if ( slot . task -> n_tokens () > n_ubatch ) {
2025-12-19 19:10:00 +08:00
send_error ( slot ,
string_format (
"input (%d tokens) is too large to process. increase the physical batch "
"size (current batch size: %d)" ,
slot . task -> n_tokens (), n_ubatch ),
ERROR_TYPE_SERVER );
2025-11-29 22:04:44 +01:00
slot . release ();
continue ;
}
if ( slot . task -> n_tokens () > slot . n_ctx ) {
2025-12-19 19:10:00 +08:00
send_error (
slot ,
string_format (
"input (%d tokens) is larger than the max context size (%d tokens). skipping" ,
slot . task -> n_tokens (), slot . n_ctx ),
ERROR_TYPE_EXCEED_CONTEXT_SIZE );
2025-11-29 22:04:44 +01:00
slot . release ();
continue ;
}
} else {
if ( slot . task -> n_tokens () >= slot . n_ctx ) {
2025-12-19 19:10:00 +08:00
send_error ( slot ,
string_format ( "request (%d tokens) exceeds the available context size (%d "
"tokens), try increasing it" ,
slot . task -> n_tokens (), slot . n_ctx ),
ERROR_TYPE_EXCEED_CONTEXT_SIZE );
2025-11-29 22:04:44 +01:00
slot . release ();
continue ;
}
if ( slot . task -> params . cache_prompt ) {
// reuse any previously computed tokens that are common with the new prompt
n_past = slot . prompt . tokens . get_common_prefix ( input_tokens );
// if there is an alora invoked, don't cache after the invocation start
if ( slot . alora_invocation_start > 0 ) {
SLT_DBG ( slot , "only caching to alora invocation start (n_past = %d, alora_invocation_start = %d) \n " , n_past , slot . alora_invocation_start );
n_past = std :: min ( n_past , slot . alora_invocation_start - 1 );
}
2025-12-08 12:43:12 +02:00
const auto n_cache_reuse = slot . task -> params . n_cache_reuse ;
const bool can_cache_reuse =
llama_memory_can_shift ( llama_get_memory ( ctx )) &&
! slot . prompt . tokens . has_mtmd ;
if ( ! can_cache_reuse && n_cache_reuse > 0 ) {
SLT_WRN ( slot , "cache reuse is not supported - ignoring n_cache_reuse = %d \n " , n_cache_reuse );
}
2025-11-29 22:04:44 +01:00
// reuse chunks from the cached prompt by shifting their KV cache in the new position
2025-12-08 12:43:12 +02:00
if ( can_cache_reuse && n_cache_reuse > 0 ) {
2025-11-29 22:04:44 +01:00
GGML_ASSERT ( ! slot . prompt . tokens . has_mtmd );
size_t head_c = n_past ; // cache
size_t head_p = n_past ; // current prompt
if ( mctx ) {
// we should never reach this
GGML_ABORT ( "not supported by multimodal" );
}
2025-12-08 12:43:12 +02:00
SLT_DBG ( slot , "trying to reuse chunks with size > %d, n_past = %d \n " , n_cache_reuse , n_past );
2025-11-29 22:04:44 +01:00
while ( head_c < slot . prompt . tokens . size () &&
head_p < input_tokens . size ()) {
size_t n_match = 0 ;
while ( head_c + n_match < slot . prompt . tokens . size () &&
head_p + n_match < input_tokens . size () &&
slot . prompt . tokens [ head_c + n_match ] == input_tokens [ head_p + n_match ]) {
n_match ++ ;
}
2025-12-08 12:43:12 +02:00
if ( n_match >= ( size_t ) n_cache_reuse ) {
2025-11-29 22:04:44 +01:00
SLT_INF ( slot , "reusing chunk with size %zu, shifting KV cache [%zu, %zu) -> [%zu, %zu) \n " , n_match , head_c , head_c + n_match , head_p , head_p + n_match );
//for (size_t i = head_p; i < head_p + n_match; i++) {
// SLT_DBG(slot, "cache token %3zu: %6d '%s'\n", i, prompt_tokens[i], common_token_to_piece(ctx, prompt_tokens[i]).c_str());
//}
const int64_t kv_shift = ( int64_t ) head_p - ( int64_t ) head_c ;
llama_memory_seq_rm ( llama_get_memory ( ctx ), slot . id , head_p , head_c );
llama_memory_seq_add ( llama_get_memory ( ctx ), slot . id , head_c , head_c + n_match , kv_shift );
for ( size_t i = 0 ; i < n_match ; i ++ ) {
slot . prompt . tokens . set_token ( head_p + i , slot . prompt . tokens [ head_c + i ]);
n_past ++ ;
}
head_c += n_match ;
head_p += n_match ;
} else {
head_c += 1 ;
}
}
SLT_DBG ( slot , "after context reuse, new n_past = %d \n " , n_past );
}
} else {
// if we don't cache the prompt, we have to remove all previous tokens
n_past = 0 ;
}
2026-02-25 15:14:27 +02:00
llama_pos pos_next = slot . prompt . tokens . pos_next ( n_past );
2025-11-29 22:04:44 +01:00
// note: when n_swa == 0, the model does not use SWA, which is equivalent to a window of 1
const auto n_swa = std :: max ( 1 , llama_model_n_swa ( model ));
// the largest pos_min required for a checkpoint to be useful
2026-02-25 15:14:27 +02:00
const auto pos_min_thold = std :: max ( 0 , pos_next - n_swa );
2025-11-29 22:04:44 +01:00
2026-02-25 15:14:27 +02:00
if ( n_past > 0 && n_past < slot . prompt . n_tokens ()) {
2025-11-29 22:04:44 +01:00
const auto pos_min = llama_memory_seq_pos_min ( llama_get_memory ( ctx ), slot . id );
if ( pos_min == - 1 ) {
SLT_ERR ( slot , "n_past = %d, slot.prompt.tokens.size() = %d, seq_id = %d, pos_min = %d \n " , n_past , ( int ) slot . prompt . tokens . size (), slot . id , pos_min );
GGML_ABORT ( "pos_min == -1, but n_past > 0 - should not happen: https://github.com/ggml-org/llama.cpp/pull/13833#discussion_r2116181237" );
}
// when the prompt prefix does not match, print the tokens around the mismatch
// this is useful for debugging prompt caching
if ( slots_debug ) {
const int np0 = std :: max < int > ( n_past - 4 , 0 );
const int np1 = std :: min < int > ( n_past + 6 , std :: min ( slot . prompt . tokens . size (), slot . task -> tokens . size ()));
std :: stringstream ss0 ;
std :: stringstream ss1 ;
std :: stringstream st0 ;
std :: stringstream st1 ;
ss0 << "old: ... " ;
ss1 << "new: ... " ;
for ( int i = np0 ; i < np1 ; i ++ ) {
if ( i == n_past ) {
ss0 << " | " ;
ss1 << " | " ;
}
{
const auto token = slot . prompt . tokens [ i ];
const auto piece = token != LLAMA_TOKEN_NULL ? common_token_to_piece ( ctx , token ) : "[mtmd]" ;
ss0 << piece ;
st0 << std :: setw ( 8 ) << token ;
}
{
const auto token = slot . task -> tokens [ i ];
const auto piece = token != LLAMA_TOKEN_NULL ? common_token_to_piece ( ctx , token ) : "[mtmd]" ;
ss1 << piece ;
st1 << std :: setw ( 8 ) << token ;
}
}
SLT_WRN ( slot , "%s \n " , ss0 . str (). c_str ());
SLT_WRN ( slot , "%s \n " , ss1 . str (). c_str ());
SLT_WRN ( slot , "%s \n " , st0 . str (). c_str ());
SLT_WRN ( slot , "%s \n " , st1 . str (). c_str ());
}
if ( pos_min > pos_min_thold ) {
SLT_WRN ( slot , "n_past = %d, slot.prompt.tokens.size() = %d, seq_id = %d, pos_min = %d, n_swa = %d \n " , n_past , ( int ) slot . prompt . tokens . size (), slot . id , pos_min , n_swa );
// search for a context checkpoint
const auto it = std :: find_if (
slot . prompt . checkpoints . rbegin (),
slot . prompt . checkpoints . rend (),
2026-03-06 11:39:26 +01:00
[ & , func_name = __func__ ]( const auto & cur ) {
2025-11-29 22:04:44 +01:00
// guarantee that a checkpoint will result in at least one token being processed [TAG_PROMPT_LOGITS]
2026-03-06 11:39:26 +01:00
LOG_INF ( "slot %12.*s: id %2d | task %d | Checking checkpoint with [%d, %d] against %d... \n " , 12 ,
func_name , ( slot ). id , (( slot ). task ? ( slot ). task -> id : - 1 ), cur . pos_min , cur . pos_max , pos_min_thold );
2025-11-29 22:04:44 +01:00
return cur . pos_min < pos_min_thold ;
}
);
bool do_reset = it == slot . prompt . checkpoints . rend ();
if ( ! do_reset ) {
// restore the context checkpoint
const size_t checkpoint_size = it -> data . size ();
const size_t n = llama_state_seq_set_data_ext ( ctx , it -> data . data (), checkpoint_size , slot . id , LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY );
if ( n != checkpoint_size ) {
2026-02-25 15:14:27 +02:00
SLT_ERR ( slot , "failed to restore context checkpoint (pos_min = %d, pos_max = %d, n_tokens = %" PRId64 ", size = %.3f MiB) \n " , it -> pos_min , it -> pos_max , it -> n_tokens , ( float ) checkpoint_size / 1024 / 1024 );
2025-11-29 22:04:44 +01:00
do_reset = true ;
//printf("[DEBUG] `do_reset` was set to `true` after failing to restore a checkpoint");
} else {
2026-02-25 15:14:27 +02:00
pos_next = std :: min ( pos_next , std :: max ( it -> pos_min + 1 , it -> pos_max ));
2026-02-26 18:20:16 +02:00
n_past = std :: min ( slot . prompt . tokens . size_up_to_pos ( pos_next ), ( size_t ) it -> n_tokens );
2026-02-25 15:14:27 +02:00
SLT_WRN ( slot , "restored context checkpoint (pos_min = %d, pos_max = %d, n_tokens = %" PRId64 ", size = %.3f MiB) \n " , it -> pos_min , it -> pos_max , it -> n_tokens , ( float ) checkpoint_size / 1024 / 1024 );
2025-11-29 22:04:44 +01:00
}
}
if ( do_reset ) {
SLT_WRN ( slot , "forcing full prompt re-processing due to lack of cache data (likely due to SWA or hybrid/recurrent memory, see %s) \n " ,
"https://github.com/ggml-org/llama.cpp/pull/13194#issuecomment-2868343055" );
2026-02-25 15:14:27 +02:00
pos_next = 0 ;
2025-11-29 22:04:44 +01:00
n_past = 0 ;
}
}
}
{
// erase any checkpoints with pos_min > pos_min_thold
for ( auto it = slot . prompt . checkpoints . begin (); it != slot . prompt . checkpoints . end ();) {
const auto & cur = * it ;
if ( cur . pos_min > pos_min_thold ) {
2026-02-25 15:14:27 +02:00
SLT_WRN ( slot , "erased invalidated context checkpoint (pos_min = %d, pos_max = %d, n_tokens = %" PRId64 ", n_swa = %d, size = %.3f MiB) \n " , cur . pos_min , cur . pos_max , cur . n_tokens , n_swa , ( float ) cur . data . size () / 1024 / 1024 );
2025-11-29 22:04:44 +01:00
it = slot . prompt . checkpoints . erase ( it );
} else {
++ it ;
}
}
}
}
// [TAG_PROMPT_LOGITS]
if ( n_past == slot . task -> n_tokens () && n_past > 0 ) {
SLT_WRN ( slot , "need to evaluate at least 1 token for each active slot (n_past = %d, task.n_tokens() = %d) \n " , n_past , slot . task -> n_tokens ());
n_past -- ;
SLT_WRN ( slot , "n_past was set to %d \n " , n_past );
}
2026-02-25 15:14:27 +02:00
slot . n_prompt_tokens_cache = n_past ;
2025-11-29 22:04:44 +01:00
slot . n_prompt_tokens_processed = 0 ;
slot . prompt . tokens . keep_first ( n_past );
2025-12-23 21:49:05 +01:00
// send initial 0% progress update if needed
// this is to signal the client that the request has started processing
if ( slot . task -> params . stream && slot . task -> params . return_progress ) {
send_partial_response ( slot , {}, true );
}
2025-11-29 22:04:44 +01:00
}
if ( ! slot . can_split ()) {
// cannot fit the prompt in the current batch - will try next iter
if ( batch . n_tokens + slot . task -> n_tokens () > n_batch ) {
continue ;
}
}
// truncate any tokens that are beyond n_past for this slot
const llama_pos p0 = slot . prompt . tokens . pos_next ();
SLT_INF ( slot , "n_tokens = %d, memory_seq_rm [%d, end) \n " , slot . prompt . n_tokens (), p0 );
if ( ! llama_memory_seq_rm ( llama_get_memory ( ctx ), slot . id , p0 , - 1 )) {
SLT_WRN ( slot , "failed to truncate tokens with position >= %d - clearing the memory \n " , p0 );
2026-01-15 16:39:17 +02:00
slot . prompt_clear ( true );
2025-11-29 22:04:44 +01:00
// there is no common part left
slot . n_prompt_tokens_cache = 0 ;
}
// check if we should process the image
if ( slot . prompt . n_tokens () < slot . task -> n_tokens () && input_tokens [ slot . prompt . n_tokens ()] == LLAMA_TOKEN_NULL ) {
// process the image
size_t n_tokens_out = 0 ;
int32_t res = input_tokens . process_chunk ( ctx , mctx , slot . prompt . n_tokens (), slot . prompt . tokens . pos_next (), slot . id , n_tokens_out );
if ( res != 0 ) {
SLT_ERR ( slot , "failed to process image, res = %d \n " , res );
send_error ( slot , "failed to process image" , ERROR_TYPE_SERVER );
slot . release ();
continue ;
}
slot . n_prompt_tokens_processed += n_tokens_out ;
// add the image chunk to cache
{
const auto & chunk = input_tokens . find_chunk ( slot . prompt . n_tokens ());
slot . prompt . tokens . push_back ( chunk . get ()); // copy
}
}
// If using an alora, there may be uncached tokens that come
// before the invocation sequence. When this happens, the
// tokens before the invocation sequence need to be
// processed without the adapter in a separate batch, then
// the adapter needs to be enabled for the remaining tokens.
if ( lora_all_alora ( slot . lora ) && slot . alora_invocation_start - 1 > slot . prompt . n_tokens ()) {
SLT_DBG ( slot , "processing pre-alora tokens without the adapter (n_tokens = %d, alora_invocation_start = %d) \n " , slot . prompt . n_tokens (), slot . alora_invocation_start );
const auto & enabled_loras = lora_get_enabled_ids ( slot . lora );
GGML_ASSERT ( enabled_loras . size () == 1 );
alora_scale = slot . lora [ enabled_loras [ 0 ]]. scale ;
slot . lora [ enabled_loras [ 0 ]]. scale = 0.0f ;
alora_disabled_id = enabled_loras [ 0 ];
}
bool do_checkpoint = params_base . n_ctx_checkpoints > 0 ;
// make checkpoints only for completion tasks
do_checkpoint = do_checkpoint && slot . task -> type == SERVER_TASK_TYPE_COMPLETION ;
// make a checkpoint of the parts of the memory that cannot be rolled back.
// checkpoints are created only if:
// - the model uses SWA and we are not using `swa_full`
// - the model architecture is marked as recurrent or hybrid
//
// TODO: try to make this conditional on the context or the memory module, instead of the model type
do_checkpoint = do_checkpoint && (
llama_model_is_recurrent ( model ) ||
llama_model_is_hybrid ( model ) ||
( llama_model_n_swa ( model ) > 0 && ! params_base . swa_full )
);
// add prompt tokens for processing in the current batch
while ( slot . prompt . n_tokens () < slot . task -> n_tokens () && batch . n_tokens < n_batch ) {
// get next token to process
llama_token cur_tok = input_tokens [ slot . prompt . n_tokens ()];
if ( cur_tok == LLAMA_TOKEN_NULL ) {
break ; // end of text chunk
}
// if this is an alora request with pre-invocation
// tokens that are not cached, we need to stop filling
// this batch at those pre-invocation tokens.
if ( alora_scale > 0 && slot . prompt . n_tokens () == slot . alora_invocation_start - 1 ) {
SLT_DBG ( slot , "stop prompt batch filling at (n_tokens = %d, alora_invocation_start = %d) \n " , slot . prompt . n_tokens (), slot . alora_invocation_start );
break ;
}
// embedding requires all tokens in the batch to be output
common_batch_add ( batch ,
cur_tok ,
slot . prompt . tokens . pos_next (),
{ slot . id },
2026-01-15 16:39:17 +02:00
slot . task -> need_embd ());
2025-11-29 22:04:44 +01:00
slot . prompt . tokens . push_back ( cur_tok );
slot . n_prompt_tokens_processed ++ ;
// process the last few tokens of the prompt separately in order to allow for a checkpoint to be created.
2026-02-08 09:40:04 +02:00
const int n_last = std :: min ( n_batch , 512 );
if ( do_checkpoint && slot . task -> n_tokens () == slot . prompt . n_tokens () + n_last ) {
2025-11-29 22:04:44 +01:00
break ;
}
}
// entire prompt has been processed
if ( slot . prompt . n_tokens () == slot . task -> n_tokens ()) {
slot . state = SLOT_STATE_DONE_PROMPT ;
GGML_ASSERT ( batch . n_tokens > 0 );
// extract the logits only for the last token
batch . logits [ batch . n_tokens - 1 ] = true ;
slot . n_decoded = 0 ;
slot . i_batch = batch . n_tokens - 1 ;
2026-01-10 00:00:41 +01:00
slot . init_sampler ();
2026-02-25 15:14:27 +02:00
SLT_INF ( slot , "prompt processing done, n_tokens = %d, batch.n_tokens = %d \n " , slot . prompt . n_tokens (), batch . n_tokens );
} else {
2026-03-06 11:39:26 +01:00
// only do non-end checkpoints if the "checkpoint every n tokens" option is set
do_checkpoint = do_checkpoint && params_base . checkpoint_every_nt > 0 ;
if ( do_checkpoint ) {
llama_pos last_checkpoint = 0 ;
if ( ! slot . prompt . checkpoints . empty ()) {
last_checkpoint = slot . prompt . checkpoints . back (). n_tokens ;
}
do_checkpoint = do_checkpoint && slot . prompt . n_tokens () - batch . n_tokens - last_checkpoint >= params_base . checkpoint_every_nt ;
if ( do_checkpoint ) {
SLT_INF ( slot , "%d tokens since last checkpoint at %d, creating new checkpoint during processing at position %d \n " , params_base . checkpoint_every_nt , last_checkpoint , slot . prompt . n_tokens ());
}
}
2026-02-25 15:14:27 +02:00
SLT_INF ( slot , "prompt processing progress, n_tokens = %d, batch.n_tokens = %d, progress = %f \n " , slot . prompt . n_tokens (), batch . n_tokens , ( float ) slot . prompt . n_tokens () / slot . task -> n_tokens ());
2025-11-29 22:04:44 +01:00
}
2026-03-06 11:39:26 +01:00
const auto pos_min = llama_memory_seq_pos_min ( llama_get_memory ( ctx ), slot . id );
const auto pos_max = llama_memory_seq_pos_max ( llama_get_memory ( ctx ), slot . id );
// no need for empty or small checkpoints
do_checkpoint = do_checkpoint && ( pos_min >= 0 && pos_max >= 64 );
// no need to create checkpoints that are too close together
do_checkpoint = do_checkpoint && ( slot . prompt . checkpoints . empty () || pos_max > slot . prompt . checkpoints . back (). pos_max + 64 );
// note: we create the checkpoint before calling llama_decode(), so the current batch is not
// yet processed and therefore it is not part of the checkpoint.
if ( do_checkpoint ) {
while ( slot . prompt . checkpoints . size () >= ( size_t ) params_base . n_ctx_checkpoints ) {
// make room for the new checkpoint, if needed
const auto & cur = slot . prompt . checkpoints . front ();
SLT_WRN ( slot ,
"erasing old context checkpoint (pos_min = %d, pos_max = %d, n_tokens = %" PRId64
", size = %.3f MiB) \n " ,
cur . pos_min , cur . pos_max , cur . n_tokens , ( float ) cur . data . size () / 1024 / 1024 );
slot . prompt . checkpoints . erase ( slot . prompt . checkpoints . begin ());
}
const size_t checkpoint_size =
llama_state_seq_get_size_ext ( ctx , slot . id , LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY );
auto & cur = slot . prompt . checkpoints . emplace_back ( server_prompt_checkpoint {
/*.pos_min = */ pos_min ,
/*.pos_max = */ pos_max ,
/*.n_tokens = */ slot . prompt . n_tokens () - batch . n_tokens ,
/*.data = */ std :: vector < uint8_t > ( checkpoint_size ),
});
llama_state_seq_get_data_ext ( ctx , cur . data . data (), checkpoint_size , slot . id ,
LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY );
SLT_WRN ( slot ,
"created context checkpoint %d of %d (pos_min = %d, pos_max = %d, n_tokens = %" PRId64
", size = %.3f MiB) \n " ,
( int ) slot . prompt . checkpoints . size (), params_base . n_ctx_checkpoints , cur . pos_min ,
cur . pos_max , cur . n_tokens , ( float ) cur . data . size () / 1024 / 1024 );
}
2025-11-29 22:04:44 +01:00
}
if ( ! slot_batched ) {
slot_batched = & slot ;
}
if ( batch . n_tokens >= n_batch ) {
break ;
}
}
}
SRV_DBG ( "decoding batch, n_tokens = %d \n " , batch . n_tokens );
if ( slot_batched ) {
// apply lora, only need to do it once per batch
common_set_adapter_lora ( ctx , slot_batched -> lora );
// if the lora is temporarily disabled for an alora, re-enable it
// for next time
if ( alora_scale > 0.0f ) {
SRV_DBG ( "re-enabling alora with scale %f \n " , alora_scale );
slot_batched -> lora [ alora_disabled_id ]. scale = alora_scale ;
}
2026-01-15 16:39:17 +02:00
llama_set_embeddings ( ctx , slot_batched -> task -> need_embd ());
2025-11-29 22:04:44 +01:00
}
2026-01-10 00:00:41 +01:00
if ( batch . n_tokens == 0 ) {
SRV_WRN ( "%s" , "no tokens to decode \n " );
}
2025-11-29 22:04:44 +01:00
int32_t i_next = 0 ;
// process the created batch of tokens
for ( int32_t i = 0 ; i < batch . n_tokens ; i = i_next ) {
const int32_t n_tokens = std :: min ( n_batch , batch . n_tokens - i );
llama_batch batch_view = {
n_tokens ,
batch . token + i ,
nullptr ,
batch . pos + i ,
batch . n_seq_id + i ,
batch . seq_id + i ,
batch . logits + i ,
};
const int ret = llama_decode ( ctx , batch_view );
metrics . on_decoded ( slots );
if ( ret != 0 ) {
{
std :: string err ;
if ( n_batch == 1 && ret == 1 ) {
// TODO: try to terminate only the largest active slot/sequence and continue with the rest
// need to remove the tokens from the current batch too
err = "Context size has been exceeded." ;
}
if ( ret == - 1 ) {
err = "Invalid input batch." ;
}
if ( ret < - 1 ) {
// TODO: update slot state based on llama_memory_seq_pos_min() and llama_memory_seq_pos_max()
err = "Compute error." ;
}
// TODO: handle ret == 2 (abort) when we start aborting
if ( ! err . empty ()) {
SRV_ERR ( "%s i = %d, n_batch = %d, ret = %d \n " , err . c_str (), i , n_batch , ret );
for ( auto & slot : slots ) {
if ( slot . is_processing ()) {
send_error ( slot , err );
slot . release ();
// note: it's complicated to keep track of how much of the current batch has been
// processed before the error occurred, so we simply clear the entire context
2026-01-15 16:39:17 +02:00
slot . prompt_clear ( false );
2025-11-29 22:04:44 +01:00
}
}
break ;
}
}
// retry with half the batch size to try to find a free slot in the KV cache
if ( ! try_clear_idle_slots ()) {
n_batch /= 2 ;
}
SRV_WRN ( "failed to find free space in the KV cache, retrying with smaller batch size, i = %d, n_batch = %d, ret = %d \n " , i , n_batch , ret );
continue ; // continue loop of n_batch
}
// move the head of the batch forward with the number of tokens we just processed
i_next = i + n_tokens ;
// on successful decode, restore the original batch size
n_batch = llama_n_batch ( ctx );
2026-01-10 00:00:41 +01:00
// handle `n_cmpl > 1` tasks - when the main prompt is processed, activate all child tasks too
2025-11-29 22:04:44 +01:00
for ( auto & slot : slots ) {
2026-01-15 17:10:28 +01:00
if ( slot . state == SLOT_STATE_DONE_PROMPT && slot . task -> is_parent ()) {
2026-01-10 00:00:41 +01:00
std :: vector < server_slot *> children ;
2025-12-06 15:54:38 +01:00
for ( auto & other : slots ) {
if ( other . state == SLOT_STATE_WAIT_OTHER && slot . task -> id == other . task -> id_parent ) {
2026-01-10 00:00:41 +01:00
children . push_back ( & other );
2025-12-06 15:54:38 +01:00
}
}
2026-01-15 17:10:28 +01:00
// all children slots should already launched by launch_slots_with_parent_task()
// copy state to the child slots
for ( auto & child : children ) {
SLT_INF ( slot , " - copying state to child %d \n " , child -> id );
2026-01-10 00:00:41 +01:00
2026-01-15 17:10:28 +01:00
GGML_ASSERT ( child -> state == SLOT_STATE_WAIT_OTHER );
2026-01-10 00:00:41 +01:00
2026-01-15 17:10:28 +01:00
slot . copy_state_to ( * child );
child -> state = SLOT_STATE_DONE_PROMPT ;
2025-12-06 15:54:38 +01:00
}
}
2026-01-10 00:00:41 +01:00
}
2025-12-06 15:54:38 +01:00
2026-01-10 00:00:41 +01:00
for ( auto & slot : slots ) {
2025-11-29 22:04:44 +01:00
// optionally send prompt processing progress
if ( slot . state == SLOT_STATE_PROCESSING_PROMPT || slot . state == SLOT_STATE_DONE_PROMPT ) {
if ( slot . task -> params . stream && slot . task -> params . return_progress ) {
send_partial_response ( slot , {}, true );
}
}
if ( slot . i_batch < ( int ) i || slot . i_batch >= ( int ) ( i + n_tokens )) {
continue ; // continue loop of slots
}
if ( slot . state == SLOT_STATE_DONE_PROMPT ) {
if ( slot . task -> type == SERVER_TASK_TYPE_EMBEDDING ) {
// prompt evaluated for embedding
send_embedding ( slot , batch_view );
slot . release ();
slot . i_batch = - 1 ;
continue ; // continue loop of slots
}
if ( slot . task -> type == SERVER_TASK_TYPE_RERANK ) {
send_rerank ( slot , batch_view );
slot . release ();
slot . i_batch = - 1 ;
continue ; // continue loop of slots
}
2026-01-15 16:39:17 +02:00
GGML_ASSERT ( slot . task -> need_sampling ());
2025-11-29 22:04:44 +01:00
// prompt evaluated for next-token prediction
slot . state = SLOT_STATE_GENERATING ;
2026-01-28 18:42:42 +01:00
if ( slot . can_speculate ()) {
common_speculative_begin ( slot . spec , slot . prompt . tokens . get_text_tokens ());
}
2025-11-29 22:04:44 +01:00
} else if ( slot . state != SLOT_STATE_GENERATING ) {
continue ; // continue loop of slots
}
2025-12-08 14:35:28 +01:00
if ( slot . i_batch_dft . size () > 0 ) {
continue ; // sample using speculative decoding
}
2025-11-29 22:04:44 +01:00
const int tok_idx = slot . i_batch - i ;
2025-12-14 10:11:13 +02:00
llama_token id = common_sampler_sample ( slot . smpl . get (), ctx , tok_idx );
2025-11-29 22:04:44 +01:00
slot . i_batch = - 1 ;
2025-12-14 10:11:13 +02:00
common_sampler_accept ( slot . smpl . get (), id , true );
2025-11-29 22:04:44 +01:00
2026-01-09 12:59:50 +02:00
// here we have synchronized the llama_context (due to the sampling above), so we can do time measurement
const int64_t t_current = ggml_time_us ();
2025-11-29 22:04:44 +01:00
slot . n_decoded += 1 ;
if ( slot . n_decoded == 1 ) {
slot . t_start_generation = t_current ;
slot . t_prompt_processing = ( slot . t_start_generation - slot . t_start_process_prompt ) / 1e3 ;
metrics . on_prompt_eval ( slot );
}
slot . t_token_generation = std :: max < int64_t > ( 1 , t_current - slot . t_start_generation ) / 1e3 ;
completion_token_output result ;
result . tok = id ;
result . text_to_send = common_token_to_piece ( ctx , result . tok , accept_special_token ( slot , result . tok ));
result . prob = 1.0f ; // TODO: set it here instead of doing inside populate_token_probs
if ( slot . task -> params . sampling . n_probs > 0 ) {
populate_token_probs ( slot , result , slot . task -> params . post_sampling_probs , params_base . special , tok_idx );
}
if ( ! process_token ( result , slot )) {
// release slot because of stop condition
slot . print_timings ();
send_final_response ( slot );
metrics . on_prediction ( slot );
slot . release ();
continue ;
}
}
2025-12-08 14:35:28 +01:00
// speculative decoding - main model sample and accept
2025-11-29 22:04:44 +01:00
for ( auto & slot : slots ) {
2025-12-08 14:35:28 +01:00
if ( slot . state != SLOT_STATE_GENERATING || slot . i_batch_dft . empty ()) {
2025-11-29 22:04:44 +01:00
continue ;
}
2026-01-10 00:00:41 +01:00
const size_t n_draft = slot . drafted . size ();
2025-11-29 22:04:44 +01:00
// the accepted tokens from the speculation
2025-12-14 10:11:13 +02:00
const auto ids = common_sampler_sample_and_accept_n ( slot . smpl . get (), ctx , slot . i_batch_dft , slot . drafted );
2025-12-08 14:35:28 +01:00
slot . i_batch_dft . clear ();
slot . drafted . clear ();
2025-11-29 22:04:44 +01:00
2026-01-09 12:59:50 +02:00
const int64_t t_current = ggml_time_us ();
2025-11-29 22:04:44 +01:00
slot . n_decoded += ids . size ();
2025-12-08 14:35:28 +01:00
slot . t_token_generation = std :: max < int64_t > ( 1 , t_current - slot . t_start_generation ) / 1e3 ;
2025-11-29 22:04:44 +01:00
// update how many tokens out of those tested were accepted
slot . n_draft_accepted += ids . size () - 1 ;
2026-01-28 18:42:42 +01:00
// inform the speculative decoding about the number of accepted tokens
common_speculative_accept ( slot . spec , ids . size () - 1 );
2025-12-08 14:35:28 +01:00
// rollback to the state before sampling the draft tokens
slot . prompt . tokens . keep_first ( slot . prompt . n_tokens () - n_draft );
// add accepted tokens to the prompt
2025-11-29 22:04:44 +01:00
slot . prompt . tokens . insert ({ ids . begin (), ids . end () - 1 });
2025-12-08 14:35:28 +01:00
slot . sampled = ids . back (); // last accepted token
2025-11-29 22:04:44 +01:00
llama_memory_seq_rm ( llama_get_memory ( ctx ), slot . id , slot . prompt . n_tokens (), - 1 );
for ( size_t i = 0 ; i < ids . size (); ++ i ) {
completion_token_output result ;
result . tok = ids [ i ];
result . text_to_send = common_token_to_piece ( ctx , result . tok , accept_special_token ( slot , result . tok ));
result . prob = 1.0f ; // set later
// TODO: set result.probs
if ( ! process_token ( result , slot )) {
slot . print_timings ();
send_final_response ( slot );
metrics . on_prediction ( slot );
slot . release ();
break ;
}
}
2025-12-20 04:57:40 -05:00
SLT_DBG ( slot , "accepted %d/%d draft tokens, new n_tokens = %d \n " , ( int ) ids . size () - 1 , ( int ) n_draft , slot . prompt . n_tokens ());
2025-11-29 22:04:44 +01:00
}
}
SRV_DBG ( "%s" , "run slots completed \n " );
}
int get_slot_n_ctx () {
return slots . back (). n_ctx ;
}
2025-12-08 17:04:38 +01:00
server_response_reader get_response_reader () {
return server_response_reader ( queue_tasks , queue_results , HTTP_POLLING_SECONDS );
}
2025-11-29 22:04:44 +01:00
};
//
// server_context (public API)
//
server_context :: server_context () : impl ( new server_context_impl ()) {}
server_context ::~ server_context () = default ;
bool server_context :: load_model ( const common_params & params ) {
return impl -> load_model ( params );
}
void server_context :: start_loop () {
2025-12-21 02:24:42 +01:00
auto & params = impl -> params_base ;
impl -> queue_tasks . start_loop ( params . sleep_idle_seconds * 1000 );
2025-11-29 22:04:44 +01:00
}
void server_context :: terminate () {
impl -> queue_tasks . terminate ();
}
llama_context * server_context :: get_llama_context () const {
return impl -> ctx ;
}
2025-12-08 17:04:38 +01:00
server_response_reader server_context :: get_response_reader () {
return impl -> get_response_reader ();
2025-11-29 22:04:44 +01:00
}
2025-12-22 14:23:34 +01:00
server_context_meta server_context :: get_meta () const {
2025-12-23 14:39:36 +01:00
auto bos_id = llama_vocab_bos ( impl -> vocab );
auto eos_id = llama_vocab_eos ( impl -> vocab );
auto bos_token_str = bos_id != LLAMA_TOKEN_NULL ? common_token_to_piece ( impl -> ctx , bos_id , true ) : "" ;
auto eos_token_str = eos_id != LLAMA_TOKEN_NULL ? common_token_to_piece ( impl -> ctx , eos_id , true ) : "" ;
2025-12-22 14:23:34 +01:00
return server_context_meta {
/* build_info */ build_info ,
/* model_name */ impl -> model_name ,
2026-02-27 07:05:23 +01:00
/* model_aliases */ impl -> model_aliases ,
/* model_tags */ impl -> model_tags ,
2025-12-22 14:23:34 +01:00
/* model_path */ impl -> params_base . model . path ,
/* has_mtmd */ impl -> mctx != nullptr ,
2026-01-19 23:28:01 +01:00
/* has_inp_image */ impl -> chat_params . allow_image ,
/* has_inp_audio */ impl -> chat_params . allow_audio ,
2025-12-22 14:23:34 +01:00
/* json_webui_settings */ impl -> json_webui_settings ,
/* slot_n_ctx */ impl -> get_slot_n_ctx (),
/* pooling_type */ llama_pooling_type ( impl -> ctx ),
2026-01-19 23:28:01 +01:00
/* chat_params */ impl -> chat_params ,
2026-01-22 21:30:06 +01:00
/* chat_template_caps */ common_chat_templates_get_caps ( impl -> chat_params . tmpls . get ()),
2025-12-22 14:23:34 +01:00
2025-12-23 14:39:36 +01:00
/* bos_token_str */ bos_token_str ,
/* eos_token_str */ eos_token_str ,
2025-12-22 14:23:34 +01:00
/* fim_pre_token */ llama_vocab_fim_pre ( impl -> vocab ),
/* fim_sub_token */ llama_vocab_fim_suf ( impl -> vocab ),
/* fim_mid_token */ llama_vocab_fim_mid ( impl -> vocab ),
2026-02-22 22:33:49 +01:00
/* fim_pad_token */ llama_vocab_fim_pad ( impl -> vocab ),
/* fim_rep_token */ llama_vocab_fim_rep ( impl -> vocab ),
/* fim_sep_token */ llama_vocab_fim_sep ( impl -> vocab ),
2025-12-22 14:23:34 +01:00
/* model_vocab_type */ llama_vocab_type ( impl -> vocab ),
/* model_vocab_n_tokens */ llama_vocab_n_tokens ( impl -> vocab ),
/* model_n_ctx_train */ llama_model_n_ctx_train ( impl -> model ),
/* model_n_embd_inp */ llama_model_n_embd ( impl -> model ),
/* model_n_params */ llama_model_n_params ( impl -> model ),
/* model_size */ llama_model_size ( impl -> model ),
2025-12-10 15:28:59 +01:00
};
}
2025-11-29 22:04:44 +01:00
// generator-like API for HTTP response generation
2025-12-21 02:24:42 +01:00
// may have bypass_sleep = true if the task does not use ctx_server
2025-11-29 22:04:44 +01:00
struct server_res_generator : server_http_res {
server_response_reader rd ;
2025-12-22 14:23:34 +01:00
server_res_generator ( server_queue & queue_tasks , server_response & queue_results , int sleep_idle_seconds , bool bypass_sleep = false )
: rd ( queue_tasks , queue_results , HTTP_POLLING_SECONDS ) {
2025-12-21 02:24:42 +01:00
// fast path in case sleeping is disabled
2025-12-22 14:23:34 +01:00
bypass_sleep |= sleep_idle_seconds < 0 ;
2025-12-21 02:24:42 +01:00
if ( ! bypass_sleep ) {
2025-12-22 14:23:34 +01:00
queue_tasks . wait_until_no_sleep ();
2025-12-21 02:24:42 +01:00
}
}
2025-11-29 22:04:44 +01:00
void ok ( const json & response_data ) {
status = 200 ;
data = safe_json_to_str ( response_data );
}
void error ( const json & error_data ) {
status = json_value ( error_data , "code" , 500 );
data = safe_json_to_str ({{ "error" , error_data }});
}
};
//
// server_routes
//
2025-12-22 14:23:34 +01:00
std :: unique_ptr < server_res_generator > server_routes :: handle_completions_impl (
const server_http_req & req ,
2025-11-29 22:04:44 +01:00
server_task_type type ,
const json & data ,
const std :: vector < raw_buffer > & files ,
task_response_type res_type ) {
GGML_ASSERT ( type == SERVER_TASK_TYPE_COMPLETION || type == SERVER_TASK_TYPE_INFILL );
2025-12-22 14:23:34 +01:00
auto res = create_response ();
2025-11-29 22:04:44 +01:00
auto completion_id = gen_chatcmplid ();
auto & rd = res -> rd ;
try {
std :: vector < server_task > tasks ;
const auto & prompt = data . at ( "prompt" );
// TODO: this log can become very long, put it behind a flag or think about a more compact format
//SRV_DBG("Prompt: %s\n", prompt.is_string() ? prompt.get<std::string>().c_str() : prompt.dump(2).c_str());
// process prompt
std :: vector < server_tokens > inputs ;
if ( res_type != TASK_RESPONSE_TYPE_NONE && ctx_server . mctx != nullptr ) {
// This is the case used by OAI compatible chat path with MTMD. TODO It can be moved to the path below.
inputs . push_back ( process_mtmd_prompt ( ctx_server . mctx , prompt . get < std :: string > (), files ));
} else {
// Everything else, including multimodal completions.
inputs = tokenize_input_prompts ( ctx_server . vocab , ctx_server . mctx , prompt , true , true );
}
2026-01-15 17:10:28 +01:00
// tasks.reserve(inputs.size()); // TODO: this is inaccurate due to child tasks
2025-11-29 22:04:44 +01:00
for ( size_t i = 0 ; i < inputs . size (); i ++ ) {
server_task task = server_task ( type );
2025-12-22 14:23:34 +01:00
task . id = rd . get_new_id ();
2025-11-29 22:04:44 +01:00
task . tokens = std :: move ( inputs [ i ]);
task . params = server_task :: params_from_json_cmpl (
2025-12-22 14:23:34 +01:00
ctx_server . vocab ,
params ,
meta -> slot_n_ctx ,
2025-11-29 22:04:44 +01:00
data );
task . id_slot = json_value ( data , "id_slot" , - 1 );
// OAI-compat
task . params . res_type = res_type ;
task . params . oaicompat_cmpl_id = completion_id ;
2025-12-22 14:23:34 +01:00
task . params . oaicompat_model = meta -> model_name ;
2025-11-29 22:04:44 +01:00
2026-01-10 00:00:41 +01:00
// prepare child tasks
2025-12-06 15:54:38 +01:00
if ( task . params . n_cmpl > 1 ) {
2026-01-15 17:10:28 +01:00
int n_children = task . params . n_cmpl - 1 ;
for ( int j = 0 ; j < n_children ; j ++ ) {
task . add_child ( task . id , rd . get_new_id ());
2025-12-06 15:54:38 +01:00
}
}
2026-01-15 17:10:28 +01:00
tasks . push_back ( std :: move ( task ));
2025-11-29 22:04:44 +01:00
}
rd . post_tasks ( std :: move ( tasks ));
} catch ( const std :: exception & e ) {
res -> error ( format_error_response ( e . what (), ERROR_TYPE_INVALID_REQUEST ));
return res ;
}
bool stream = json_value ( data , "stream" , false );
if ( ! stream ) {
// non-stream, wait for the results
2025-12-22 14:23:34 +01:00
auto all_results = rd . wait_for_all ( req . should_stop );
2025-11-29 22:04:44 +01:00
if ( all_results . is_terminated ) {
return res ; // connection is closed
} else if ( all_results . error ) {
res -> error ( all_results . error -> to_json ());
return res ;
} else {
json arr = json :: array ();
for ( auto & res : all_results . results ) {
GGML_ASSERT ( dynamic_cast < server_task_result_cmpl_final *> ( res . get ()) != nullptr );
arr . push_back ( res -> to_json ());
}
2025-12-06 15:54:38 +01:00
GGML_ASSERT ( ! arr . empty () && "empty results" );
if ( arr . size () == 1 ) {
// if single request, return single object instead of array
res -> ok ( arr [ 0 ]);
} else if ( res_type == TASK_RESPONSE_TYPE_OAI_CHAT || res_type == TASK_RESPONSE_TYPE_OAI_CMPL ) {
// if multiple results in OAI format, we need to re-format them
json & choices = arr [ 0 ][ "choices" ];
for ( size_t i = 1 ; i < arr . size (); i ++ ) {
choices . push_back ( std :: move ( arr [ i ][ "choices" ][ 0 ]));
}
res -> ok ( arr [ 0 ]);
} else {
// multi-results, non-OAI compat
res -> ok ( arr );
}
2025-11-29 22:04:44 +01:00
}
} else {
// in streaming mode, the first error must be treated as non-stream response
// this is to match the OAI API behavior
// ref: https://github.com/ggml-org/llama.cpp/pull/16486#discussion_r2419657309
2025-12-29 15:34:41 +02:00
auto first_result = rd . next ( req . should_stop );
2025-11-29 22:04:44 +01:00
if ( first_result == nullptr ) {
2025-12-29 15:34:41 +02:00
GGML_ASSERT ( req . should_stop ());
2025-11-29 22:04:44 +01:00
return res ; // connection is closed
2025-12-29 15:34:41 +02:00
}
if ( first_result -> is_error ()) {
2025-11-29 22:04:44 +01:00
res -> error ( first_result -> to_json ());
return res ;
}
2025-12-29 15:34:41 +02:00
GGML_ASSERT (
dynamic_cast < server_task_result_cmpl_partial *> ( first_result . get ()) != nullptr ||
dynamic_cast < server_task_result_cmpl_final *> ( first_result . get ()) != nullptr
);
2025-11-29 22:04:44 +01:00
// next responses are streamed
2025-12-04 15:46:08 +01:00
// to be sent immediately
json first_result_json = first_result -> to_json ();
2025-11-29 22:04:44 +01:00
if ( res_type == TASK_RESPONSE_TYPE_ANTHROPIC ) {
2025-12-04 15:46:08 +01:00
res -> data = format_anthropic_sse ( first_result_json );
2026-01-22 01:47:23 +09:00
} else if ( res_type == TASK_RESPONSE_TYPE_OAI_RESP ) {
res -> data = format_oai_resp_sse ( first_result_json );
2025-11-29 22:04:44 +01:00
} else {
2025-12-04 15:46:08 +01:00
res -> data = format_oai_sse ( first_result_json );
2025-11-29 22:04:44 +01:00
}
res -> status = 200 ;
res -> content_type = "text/event-stream" ;
2025-12-22 14:23:34 +01:00
res -> next = [ res_this = res . get (), res_type , & req ]( std :: string & output ) -> bool {
2025-12-04 15:46:08 +01:00
static auto format_error = []( task_response_type res_type , const json & res_json ) {
2025-11-29 22:04:44 +01:00
if ( res_type == TASK_RESPONSE_TYPE_ANTHROPIC ) {
2025-12-04 15:46:08 +01:00
return format_anthropic_sse ({
2025-11-29 22:04:44 +01:00
{ "event" , "error" },
{ "data" , res_json },
});
} else {
2025-12-04 15:46:08 +01:00
return format_oai_sse ( json {{ "error" , res_json }});
2025-11-29 22:04:44 +01:00
}
2025-12-04 15:46:08 +01:00
};
2025-11-29 22:04:44 +01:00
2025-12-04 15:46:08 +01:00
try {
2025-12-22 14:23:34 +01:00
if ( req . should_stop ()) {
2025-12-04 15:46:08 +01:00
SRV_DBG ( "%s" , "stopping streaming due to should_stop condition \n " );
return false ; // should_stop condition met
}
if ( ! res_this -> data . empty ()) {
// flush the first chunk
output = std :: move ( res_this -> data );
res_this -> data . clear ();
return true ;
}
server_response_reader & rd = res_this -> rd ;
// check if there is more data
if ( ! rd . has_next ()) {
2026-01-22 01:47:23 +09:00
switch ( res_type ) {
case TASK_RESPONSE_TYPE_NONE :
case TASK_RESPONSE_TYPE_OAI_RESP :
case TASK_RESPONSE_TYPE_ANTHROPIC :
output = "" ;
break ;
default :
output = "data: [DONE] \n\n " ;
break ;
2025-12-04 15:46:08 +01:00
}
SRV_DBG ( "%s" , "all results received, terminating stream \n " );
return false ; // no more data, terminate
}
// receive subsequent results
2025-12-22 14:23:34 +01:00
auto result = rd . next ( req . should_stop );
2025-12-04 15:46:08 +01:00
if ( result == nullptr ) {
SRV_DBG ( "%s" , "stopping streaming due to should_stop condition \n " );
2025-12-29 15:34:41 +02:00
GGML_ASSERT ( req . should_stop ());
2025-12-04 15:46:08 +01:00
return false ; // should_stop condition met
}
// send the results
if ( result -> is_error ()) {
json res_json = result -> to_json ();
output = format_error ( res_type , res_json );
SRV_DBG ( "%s" , "error received during streaming, terminating stream \n " );
return false ; // terminate on error
} else {
GGML_ASSERT (
dynamic_cast < server_task_result_cmpl_partial *> ( result . get ()) != nullptr
|| dynamic_cast < server_task_result_cmpl_final *> ( result . get ()) != nullptr
);
json res_json = result -> to_json ();
if ( res_type == TASK_RESPONSE_TYPE_ANTHROPIC ) {
output = format_anthropic_sse ( res_json );
2026-01-22 01:47:23 +09:00
} else if ( res_type == TASK_RESPONSE_TYPE_OAI_RESP ) {
output = format_oai_resp_sse ( res_json );
2025-12-04 15:46:08 +01:00
} else {
output = format_oai_sse ( res_json );
}
}
// has next data, continue
return true ;
} catch ( const std :: exception & e ) {
json error_json = format_error_response ( e . what (), ERROR_TYPE_SERVER );
output = format_error ( res_type , error_json );
// terminate on exception
return false ;
}
2025-11-29 22:04:44 +01:00
};
}
return res ;
}
2025-12-22 14:23:34 +01:00
std :: unique_ptr < server_res_generator > server_routes :: create_response ( bool bypass_sleep ) {
return std :: make_unique < server_res_generator > ( queue_tasks , queue_results , params . sleep_idle_seconds , bypass_sleep );
}
server_routes :: server_routes ( const common_params & params , server_context & ctx_server )
: params ( params ),
ctx_server ( * ctx_server . impl ),
queue_tasks ( ctx_server . impl -> queue_tasks ),
queue_results ( ctx_server . impl -> queue_results ) {
init_routes ();
}
2025-11-29 22:04:44 +01:00
void server_routes :: init_routes () {
2025-12-22 14:23:34 +01:00
// IMPORTANT: all lambda functions must start with create_response()
2025-12-21 02:24:42 +01:00
// this is to ensure that the server_res_generator can handle sleeping case correctly
2025-11-29 22:04:44 +01:00
this -> get_health = [ this ]( const server_http_req & ) {
// error and loading states are handled by middleware
2025-12-22 14:23:34 +01:00
auto res = create_response ( true );
// this endpoint can be accessed during sleeping
// the next LOC is to avoid someone accidentally use ctx_server
2026-01-19 23:28:01 +01:00
bool ctx_server ; // do NOT delete this line
GGML_UNUSED ( ctx_server );
2025-12-22 14:23:34 +01:00
2025-11-29 22:04:44 +01:00
res -> ok ({{ "status" , "ok" }});
return res ;
};
2025-12-22 14:23:34 +01:00
this -> get_metrics = [ this ]( const server_http_req & req ) {
auto res = create_response ();
2025-11-29 22:04:44 +01:00
if ( ! params . endpoint_metrics ) {
res -> error ( format_error_response ( "This server does not support metrics endpoint. Start it with `--metrics`" , ERROR_TYPE_NOT_SUPPORTED ));
return res ;
}
// request slots data using task queue
{
server_task task ( SERVER_TASK_TYPE_METRICS );
2025-12-22 14:23:34 +01:00
task . id = res -> rd . get_new_id ();
res -> rd . post_task ( std :: move ( task ), true ); // high-priority task
2025-11-29 22:04:44 +01:00
}
// get the result
2025-12-22 14:23:34 +01:00
auto result = res -> rd . next ( req . should_stop );
2025-12-29 15:34:41 +02:00
if ( ! result ) {
// connection was closed
GGML_ASSERT ( req . should_stop ());
return res ;
}
2025-11-29 22:04:44 +01:00
if ( result -> is_error ()) {
res -> error ( result -> to_json ());
return res ;
}
// TODO: get rid of this dynamic_cast
auto res_task = dynamic_cast < server_task_result_metrics *> ( result . get ());
GGML_ASSERT ( res_task != nullptr );
// metrics definition: https://prometheus.io/docs/practices/naming/#metric-names
json all_metrics_def = json {
{ "counter" , {{
{ "name" , "prompt_tokens_total" },
{ "help" , "Number of prompt tokens processed." },
{ "value" , ( uint64_t ) res_task -> n_prompt_tokens_processed_total }
}, {
{ "name" , "prompt_seconds_total" },
{ "help" , "Prompt process time" },
{ "value" , ( uint64_t ) res_task -> t_prompt_processing_total / 1.e3 }
}, {
{ "name" , "tokens_predicted_total" },
{ "help" , "Number of generation tokens processed." },
{ "value" , ( uint64_t ) res_task -> n_tokens_predicted_total }
}, {
{ "name" , "tokens_predicted_seconds_total" },
{ "help" , "Predict process time" },
{ "value" , ( uint64_t ) res_task -> t_tokens_generation_total / 1.e3 }
}, {
{ "name" , "n_decode_total" },
{ "help" , "Total number of llama_decode() calls" },
{ "value" , res_task -> n_decode_total }
}, {
{ "name" , "n_tokens_max" },
{ "help" , "Largest observed n_tokens." },
{ "value" , res_task -> n_tokens_max }
}, {
{ "name" , "n_busy_slots_per_decode" },
{ "help" , "Average number of busy slots per llama_decode() call" },
{ "value" , ( float ) res_task -> n_busy_slots_total / std :: max (( float ) res_task -> n_decode_total , 1.f )}
}}},
{ "gauge" , {{
{ "name" , "prompt_tokens_seconds" },
{ "help" , "Average prompt throughput in tokens/s." },
{ "value" , res_task -> n_prompt_tokens_processed ? 1.e3 / res_task -> t_prompt_processing * res_task -> n_prompt_tokens_processed : 0. }
},{
{ "name" , "predicted_tokens_seconds" },
{ "help" , "Average generation throughput in tokens/s." },
{ "value" , res_task -> n_tokens_predicted ? 1.e3 / res_task -> t_tokens_generation * res_task -> n_tokens_predicted : 0. }
},{
{ "name" , "requests_processing" },
{ "help" , "Number of requests processing." },
{ "value" , ( uint64_t ) res_task -> n_processing_slots }
},{
{ "name" , "requests_deferred" },
{ "help" , "Number of requests deferred." },
{ "value" , ( uint64_t ) res_task -> n_tasks_deferred }
}}}
};
std :: stringstream prometheus ;
for ( const auto & el : all_metrics_def . items ()) {
const auto & type = el . key ();
const auto & metrics_def = el . value ();
for ( const auto & metric_def : metrics_def ) {
const std :: string name = metric_def . at ( "name" );
const std :: string help = metric_def . at ( "help" );
auto value = json_value ( metric_def , "value" , 0. );
prometheus << "# HELP llamacpp:" << name << " " << help << " \n "
<< "# TYPE llamacpp:" << name << " " << type << " \n "
<< "llamacpp:" << name << " " << value << " \n " ;
}
}
res -> headers [ "Process-Start-Time-Unix" ] = std :: to_string ( res_task -> t_start );
res -> content_type = "text/plain; version=0.0.4" ;
res -> status = 200 ;
res -> data = prometheus . str ();
return res ;
};
this -> get_slots = [ this ]( const server_http_req & req ) {
2025-12-22 14:23:34 +01:00
auto res = create_response ();
2025-11-29 22:04:44 +01:00
if ( ! params . endpoint_slots ) {
res -> error ( format_error_response ( "This server does not support slots endpoint. Start it with `--slots`" , ERROR_TYPE_NOT_SUPPORTED ));
return res ;
}
// request slots data using task queue
{
server_task task ( SERVER_TASK_TYPE_METRICS );
2025-12-22 14:23:34 +01:00
task . id = res -> rd . get_new_id ();
res -> rd . post_task ( std :: move ( task ), true ); // high-priority task
2025-11-29 22:04:44 +01:00
}
// get the result
2025-12-22 14:23:34 +01:00
auto result = res -> rd . next ( req . should_stop );
2025-12-29 15:34:41 +02:00
if ( ! result ) {
// connection was closed
GGML_ASSERT ( req . should_stop ());
return res ;
}
2025-11-29 22:04:44 +01:00
if ( result -> is_error ()) {
res -> error ( result -> to_json ());
return res ;
}
// TODO: get rid of this dynamic_cast
2026-01-30 19:46:10 +02:00
auto * res_task = dynamic_cast < server_task_result_metrics *> ( result . get ());
2025-11-29 22:04:44 +01:00
GGML_ASSERT ( res_task != nullptr );
// optionally return "fail_on_no_slot" error
if ( ! req . get_param ( "fail_on_no_slot" ). empty ()) {
if ( res_task -> n_idle_slots == 0 ) {
res -> error ( format_error_response ( "no slot available" , ERROR_TYPE_UNAVAILABLE ));
return res ;
}
}
res -> ok ( res_task -> slots_data );
return res ;
};
this -> post_slots = [ this ]( const server_http_req & req ) {
2025-12-22 14:23:34 +01:00
auto res = create_response ();
2025-11-29 22:04:44 +01:00
if ( params . slot_save_path . empty ()) {
res -> error ( format_error_response ( "This server does not support slots action. Start it with `--slot-save-path`" , ERROR_TYPE_NOT_SUPPORTED ));
return res ;
}
std :: string id_slot_str = req . get_param ( "id_slot" );
2026-01-30 19:46:10 +02:00
int id_slot ;
2025-11-29 22:04:44 +01:00
try {
id_slot = std :: stoi ( id_slot_str );
} catch ( const std :: exception & ) {
res -> error ( format_error_response ( "Invalid slot ID" , ERROR_TYPE_INVALID_REQUEST ));
return res ;
}
std :: string action = req . get_param ( "action" );
if ( action == "save" ) {
return handle_slots_save ( req , id_slot );
}
2026-01-30 19:46:10 +02:00
if ( action == "restore" ) {
return handle_slots_restore ( req , id_slot );
}
if ( action == "erase" ) {
return handle_slots_erase ( req , id_slot );
}
res -> error ( format_error_response ( "Invalid action" , ERROR_TYPE_INVALID_REQUEST ));
return res ;
2025-11-29 22:04:44 +01:00
};
this -> get_props = [ this ]( const server_http_req & ) {
2025-12-22 14:23:34 +01:00
auto res = create_response ( true );
// this endpoint can be accessed during sleeping
// the next LOC is to avoid someone accidentally use ctx_server
2026-01-19 23:28:01 +01:00
bool ctx_server ; // do NOT delete this line
GGML_UNUSED ( ctx_server );
2025-12-22 14:23:34 +01:00
task_params tparams ;
tparams . sampling = params . sampling ;
json default_generation_settings_for_props = json {
{ "params" , tparams . to_json ( true ) },
{ "n_ctx" , meta -> slot_n_ctx },
};
2026-01-19 23:28:01 +01:00
std :: string tmpl_default = common_chat_templates_source ( meta -> chat_params . tmpls . get (), "" );
std :: string tmpl_tools = common_chat_templates_source ( meta -> chat_params . tmpls . get (), "tool_use" );
2025-12-22 14:23:34 +01:00
json props = {
{ "default_generation_settings" , default_generation_settings_for_props },
{ "total_slots" , params . n_parallel },
{ "model_alias" , meta -> model_name },
{ "model_path" , meta -> model_path },
{ "modalities" , json {
{ "vision" , meta -> has_inp_image },
{ "audio" , meta -> has_inp_audio },
} },
{ "endpoint_slots" , params . endpoint_slots },
{ "endpoint_props" , params . endpoint_props },
{ "endpoint_metrics" , params . endpoint_metrics },
{ "webui" , params . webui },
{ "webui_settings" , meta -> json_webui_settings },
2026-01-19 23:28:01 +01:00
{ "chat_template" , tmpl_default },
2026-01-22 21:30:06 +01:00
{ "chat_template_caps" , meta -> chat_template_caps },
2025-12-22 14:23:34 +01:00
{ "bos_token" , meta -> bos_token_str },
{ "eos_token" , meta -> eos_token_str },
{ "build_info" , meta -> build_info },
{ "is_sleeping" , queue_tasks . is_sleeping () },
};
if ( params . use_jinja ) {
2026-01-19 23:28:01 +01:00
if ( ! tmpl_tools . empty ()) {
props [ "chat_template_tool_use" ] = tmpl_tools ;
2025-12-22 14:23:34 +01:00
}
}
2025-12-21 02:24:42 +01:00
res -> ok ( props );
2025-11-29 22:04:44 +01:00
return res ;
};
this -> post_props = [ this ]( const server_http_req & ) {
2025-12-22 14:23:34 +01:00
auto res = create_response ();
2025-11-29 22:04:44 +01:00
if ( ! params . endpoint_props ) {
res -> error ( format_error_response ( "This server does not support changing global properties. Start it with `--props`" , ERROR_TYPE_NOT_SUPPORTED ));
return res ;
}
// update any props here
res -> ok ({{ "success" , true }});
return res ;
};
this -> get_api_show = [ this ]( const server_http_req & ) {
2025-12-22 14:23:34 +01:00
auto res = create_response ();
2026-01-19 23:28:01 +01:00
std :: string tmpl_default = common_chat_templates_source ( meta -> chat_params . tmpls . get (), "" );
2025-11-29 22:04:44 +01:00
json data = {
{
"model_info" , {
2025-12-22 14:23:34 +01:00
{ "llama.context_length" , meta -> slot_n_ctx },
2025-11-29 22:04:44 +01:00
}
},
{ "modelfile" , "" },
{ "parameters" , "" },
2026-01-19 23:28:01 +01:00
{ "template" , tmpl_default },
2025-11-29 22:04:44 +01:00
{ "details" , {
{ "parent_model" , "" },
{ "format" , "gguf" },
{ "family" , "" },
{ "families" , { "" }},
{ "parameter_size" , "" },
{ "quantization_level" , "" }
}},
{ "model_info" , "" },
2025-12-22 14:23:34 +01:00
{ "capabilities" , meta -> has_mtmd ? json ({ "completion" , "multimodal" }) : json ({ "completion" })}
2025-11-29 22:04:44 +01:00
};
res -> ok ( data );
return res ;
};
this -> post_infill = [ this ]( const server_http_req & req ) {
2025-12-22 14:23:34 +01:00
auto res = create_response ();
2025-11-29 22:04:44 +01:00
// check model compatibility
std :: string err ;
if ( llama_vocab_fim_pre ( ctx_server . vocab ) == LLAMA_TOKEN_NULL ) {
err += "prefix token is missing. " ;
}
if ( llama_vocab_fim_suf ( ctx_server . vocab ) == LLAMA_TOKEN_NULL ) {
err += "suffix token is missing. " ;
}
if ( llama_vocab_fim_mid ( ctx_server . vocab ) == LLAMA_TOKEN_NULL ) {
err += "middle token is missing. " ;
}
if ( ! err . empty ()) {
res -> error ( format_error_response ( string_format ( "Infill is not supported by this model: %s" , err . c_str ()), ERROR_TYPE_NOT_SUPPORTED ));
return res ;
}
// validate input
json data = json :: parse ( req . body );
if ( data . contains ( "prompt" ) && ! data . at ( "prompt" ). is_string ()) {
// prompt is optional
res -> error ( format_error_response ( " \" prompt \" must be a string" , ERROR_TYPE_INVALID_REQUEST ));
}
if ( ! data . contains ( "input_prefix" )) {
res -> error ( format_error_response ( " \" input_prefix \" is required" , ERROR_TYPE_INVALID_REQUEST ));
}
if ( ! data . contains ( "input_suffix" )) {
res -> error ( format_error_response ( " \" input_suffix \" is required" , ERROR_TYPE_INVALID_REQUEST ));
}
if ( data . contains ( "input_extra" ) && ! data . at ( "input_extra" ). is_array ()) {
// input_extra is optional
res -> error ( format_error_response ( " \" input_extra \" must be an array of { \" filename \" : string, \" text \" : string}" , ERROR_TYPE_INVALID_REQUEST ));
return res ;
}
json input_extra = json_value ( data , "input_extra" , json :: array ());
for ( const auto & chunk : input_extra ) {
// { "text": string, "filename": string }
if ( ! chunk . contains ( "text" ) || ! chunk . at ( "text" ). is_string ()) {
res -> error ( format_error_response ( "extra_context chunk must contain a \" text \" field with a string value" , ERROR_TYPE_INVALID_REQUEST ));
return res ;
}
// filename is optional
if ( chunk . contains ( "filename" ) && ! chunk . at ( "filename" ). is_string ()) {
res -> error ( format_error_response ( "extra_context chunk's \" filename \" field must be a string" , ERROR_TYPE_INVALID_REQUEST ));
return res ;
}
}
data [ "input_extra" ] = input_extra ; // default to empty array if it's not exist
std :: string prompt = json_value ( data , "prompt" , std :: string ());
std :: vector < server_tokens > tokenized_prompts = tokenize_input_prompts ( ctx_server . vocab , ctx_server . mctx , prompt , false , true );
SRV_DBG ( "creating infill tasks, n_prompts = %d \n " , ( int ) tokenized_prompts . size ());
data [ "prompt" ] = format_prompt_infill (
ctx_server . vocab ,
data . at ( "input_prefix" ),
data . at ( "input_suffix" ),
data . at ( "input_extra" ),
2025-12-22 14:23:34 +01:00
params . n_batch ,
params . n_predict ,
meta -> slot_n_ctx ,
params . spm_infill ,
2025-11-29 22:04:44 +01:00
tokenized_prompts [ 0 ]. get_text_tokens () // TODO: this could maybe be multimodal.
);
std :: vector < raw_buffer > files ; // dummy
return handle_completions_impl (
2025-12-22 14:23:34 +01:00
req ,
2025-11-29 22:04:44 +01:00
SERVER_TASK_TYPE_INFILL ,
data ,
files ,
TASK_RESPONSE_TYPE_NONE ); // infill is not OAI compatible
};
this -> post_completions = [ this ]( const server_http_req & req ) {
2025-12-22 14:23:34 +01:00
auto res = create_response ();
2025-11-29 22:04:44 +01:00
std :: vector < raw_buffer > files ; // dummy
const json body = json :: parse ( req . body );
return handle_completions_impl (
2025-12-22 14:23:34 +01:00
req ,
2025-11-29 22:04:44 +01:00
SERVER_TASK_TYPE_COMPLETION ,
body ,
files ,
TASK_RESPONSE_TYPE_NONE );
};
this -> post_completions_oai = [ this ]( const server_http_req & req ) {
2025-12-22 14:23:34 +01:00
auto res = create_response ();
2025-11-29 22:04:44 +01:00
std :: vector < raw_buffer > files ; // dummy
const json body = json :: parse ( req . body );
return handle_completions_impl (
2025-12-22 14:23:34 +01:00
req ,
2025-11-29 22:04:44 +01:00
SERVER_TASK_TYPE_COMPLETION ,
body ,
files ,
TASK_RESPONSE_TYPE_OAI_CMPL );
};
this -> post_chat_completions = [ this ]( const server_http_req & req ) {
2025-12-22 14:23:34 +01:00
auto res = create_response ();
2025-11-29 22:04:44 +01:00
std :: vector < raw_buffer > files ;
json body = json :: parse ( req . body );
json body_parsed = oaicompat_chat_params_parse (
body ,
2026-01-19 23:28:01 +01:00
meta -> chat_params ,
2025-11-29 22:04:44 +01:00
files );
return handle_completions_impl (
2025-12-22 14:23:34 +01:00
req ,
2025-11-29 22:04:44 +01:00
SERVER_TASK_TYPE_COMPLETION ,
body_parsed ,
files ,
TASK_RESPONSE_TYPE_OAI_CHAT );
};
2026-01-22 01:47:23 +09:00
this -> post_responses_oai = [ this ]( const server_http_req & req ) {
auto res = create_response ();
std :: vector < raw_buffer > files ;
json body = convert_responses_to_chatcmpl ( json :: parse ( req . body ));
2026-02-10 00:22:57 +09:00
SRV_DBG ( "%s \n " , "Request converted: OpenAI Responses -> OpenAI Chat Completions" );
SRV_DBG ( "converted request: %s \n " , body . dump (). c_str ());
2026-01-22 01:47:23 +09:00
json body_parsed = oaicompat_chat_params_parse (
body ,
meta -> chat_params ,
files );
return handle_completions_impl (
req ,
SERVER_TASK_TYPE_COMPLETION ,
body_parsed ,
files ,
TASK_RESPONSE_TYPE_OAI_RESP );
};
2025-11-29 22:04:44 +01:00
this -> post_anthropic_messages = [ this ]( const server_http_req & req ) {
2025-12-22 14:23:34 +01:00
auto res = create_response ();
2025-11-29 22:04:44 +01:00
std :: vector < raw_buffer > files ;
json body = convert_anthropic_to_oai ( json :: parse ( req . body ));
2026-02-10 00:22:57 +09:00
SRV_DBG ( "%s \n " , "Request converted: Anthropic -> OpenAI Chat Completions" );
SRV_DBG ( "converted request: %s \n " , body . dump (). c_str ());
2025-11-29 22:04:44 +01:00
json body_parsed = oaicompat_chat_params_parse (
body ,
2026-01-19 23:28:01 +01:00
meta -> chat_params ,
2025-11-29 22:04:44 +01:00
files );
return handle_completions_impl (
2025-12-22 14:23:34 +01:00
req ,
2025-11-29 22:04:44 +01:00
SERVER_TASK_TYPE_COMPLETION ,
body_parsed ,
files ,
TASK_RESPONSE_TYPE_ANTHROPIC );
};
this -> post_anthropic_count_tokens = [ this ]( const server_http_req & req ) {
2025-12-22 14:23:34 +01:00
auto res = create_response ();
2025-11-29 22:04:44 +01:00
std :: vector < raw_buffer > files ;
json body = convert_anthropic_to_oai ( json :: parse ( req . body ));
2026-02-10 00:22:57 +09:00
SRV_DBG ( "%s \n " , "Request converted: Anthropic -> OpenAI Chat Completions" );
SRV_DBG ( "converted request: %s \n " , body . dump (). c_str ());
2025-11-29 22:04:44 +01:00
json body_parsed = oaicompat_chat_params_parse (
body ,
2026-01-19 23:28:01 +01:00
meta -> chat_params ,
2025-11-29 22:04:44 +01:00
files );
json prompt = body_parsed . at ( "prompt" );
llama_tokens tokens = tokenize_mixed ( ctx_server . vocab , prompt , true , true );
res -> ok ({{ "input_tokens" , static_cast < int > ( tokens . size ())}});
return res ;
};
// same with handle_chat_completions, but without inference part
this -> post_apply_template = [ this ]( const server_http_req & req ) {
2025-12-22 14:23:34 +01:00
auto res = create_response ();
2025-11-29 22:04:44 +01:00
std :: vector < raw_buffer > files ; // dummy, unused
json body = json :: parse ( req . body );
json data = oaicompat_chat_params_parse (
body ,
2026-01-19 23:28:01 +01:00
meta -> chat_params ,
2025-11-29 22:04:44 +01:00
files );
res -> ok ({{ "prompt" , std :: move ( data . at ( "prompt" )) }});
return res ;
};
this -> get_models = [ this ]( const server_http_req & ) {
2025-12-22 14:23:34 +01:00
auto res = create_response ( true );
// this endpoint can be accessed during sleeping
// the next LOC is to avoid someone accidentally use ctx_server
2026-01-19 23:28:01 +01:00
bool ctx_server ; // do NOT delete this line
GGML_UNUSED ( ctx_server );
2025-12-22 14:23:34 +01:00
2025-11-29 22:04:44 +01:00
json models = {
{ "models" , {
{
2025-12-22 14:23:34 +01:00
{ "name" , meta -> model_name },
{ "model" , meta -> model_name },
2025-11-29 22:04:44 +01:00
{ "modified_at" , "" },
{ "size" , "" },
{ "digest" , "" }, // dummy value, llama.cpp does not support managing model file's hash
{ "type" , "model" },
{ "description" , "" },
{ "tags" , { "" }},
2025-12-22 14:23:34 +01:00
{ "capabilities" , meta -> has_mtmd ? json ({ "completion" , "multimodal" }) : json ({ "completion" })},
2025-11-29 22:04:44 +01:00
{ "parameters" , "" },
{ "details" , {
{ "parent_model" , "" },
{ "format" , "gguf" },
{ "family" , "" },
{ "families" , { "" }},
{ "parameter_size" , "" },
{ "quantization_level" , "" }
}}
}
}},
{ "object" , "list" },
{ "data" , {
{
2025-12-22 14:23:34 +01:00
{ "id" , meta -> model_name },
2026-02-27 07:05:23 +01:00
{ "aliases" , meta -> model_aliases },
{ "tags" , meta -> model_tags },
2025-11-29 22:04:44 +01:00
{ "object" , "model" },
{ "created" , std :: time ( 0 )},
{ "owned_by" , "llamacpp" },
2025-12-22 14:23:34 +01:00
{ "meta" , {
{ "vocab_type" , meta -> model_vocab_type },
{ "n_vocab" , meta -> model_vocab_n_tokens },
{ "n_ctx_train" , meta -> model_n_ctx_train },
{ "n_embd" , meta -> model_n_embd_inp },
{ "n_params" , meta -> model_n_params },
{ "size" , meta -> model_size },
}},
2025-11-29 22:04:44 +01:00
},
}}
};
res -> ok ( models );
return res ;
};
this -> post_tokenize = [ this ]( const server_http_req & req ) {
2025-12-22 14:23:34 +01:00
auto res = create_response ();
2025-11-29 22:04:44 +01:00
const json body = json :: parse ( req . body );
json tokens_response = json :: array ();
if ( body . count ( "content" ) != 0 ) {
const bool add_special = json_value ( body , "add_special" , false );
const bool parse_special = json_value ( body , "parse_special" , true );
const bool with_pieces = json_value ( body , "with_pieces" , false );
llama_tokens tokens = tokenize_mixed ( ctx_server . vocab , body . at ( "content" ), add_special , parse_special );
if ( with_pieces ) {
for ( const auto & token : tokens ) {
2025-12-22 14:23:34 +01:00
std :: string piece = common_token_to_piece ( ctx_server . vocab , token );
2025-11-29 22:04:44 +01:00
json piece_json ;
// Check if the piece is valid UTF-8
if ( is_valid_utf8 ( piece )) {
piece_json = piece ;
} else {
// If not valid UTF-8, store as array of byte values
piece_json = json :: array ();
for ( unsigned char c : piece ) {
piece_json . push_back ( static_cast < int > ( c ));
}
}
tokens_response . push_back ({
{ "id" , token },
{ "piece" , piece_json }
});
}
} else {
tokens_response = tokens ;
}
}
res -> ok ( json {{ "tokens" , std :: move ( tokens_response )}});
return res ;
};
this -> post_detokenize = [ this ]( const server_http_req & req ) {
2025-12-22 14:23:34 +01:00
auto res = create_response ();
2025-11-29 22:04:44 +01:00
const json body = json :: parse ( req . body );
std :: string content ;
if ( body . count ( "tokens" ) != 0 ) {
const llama_tokens tokens = body . at ( "tokens" );
2025-12-22 14:23:34 +01:00
content = tokens_to_str ( ctx_server . vocab , tokens );
2025-11-29 22:04:44 +01:00
}
res -> ok ( json {{ "content" , std :: move ( content )}});
return res ;
};
this -> post_embeddings = [ this ]( const server_http_req & req ) {
return handle_embeddings_impl ( req , TASK_RESPONSE_TYPE_NONE );
};
this -> post_embeddings_oai = [ this ]( const server_http_req & req ) {
return handle_embeddings_impl ( req , TASK_RESPONSE_TYPE_OAI_EMBD );
};
this -> post_rerank = [ this ]( const server_http_req & req ) {
2025-12-22 14:23:34 +01:00
auto res = create_response ();
if ( ! params . embedding || params . pooling_type != LLAMA_POOLING_TYPE_RANK ) {
2025-11-29 22:04:44 +01:00
res -> error ( format_error_response ( "This server does not support reranking. Start it with `--reranking`" , ERROR_TYPE_NOT_SUPPORTED ));
return res ;
}
const json body = json :: parse ( req . body );
// if true, use TEI API format, otherwise use Jina API format
// Jina: https://jina.ai/reranker/
// TEI: https://huggingface.github.io/text-embeddings-inference/#/Text%20Embeddings%20Inference/rerank
bool is_tei_format = body . contains ( "texts" );
json query ;
if ( body . count ( "query" ) == 1 ) {
query = body . at ( "query" );
if ( ! query . is_string ()) {
res -> error ( format_error_response ( " \" query \" must be a string" , ERROR_TYPE_INVALID_REQUEST ));
return res ;
}
} else {
res -> error ( format_error_response ( " \" query \" must be provided" , ERROR_TYPE_INVALID_REQUEST ));
return res ;
}
std :: vector < std :: string > documents = json_value ( body , "documents" ,
json_value ( body , "texts" , std :: vector < std :: string > ()));
if ( documents . empty ()) {
res -> error ( format_error_response ( " \" documents \" must be a non-empty string array" , ERROR_TYPE_INVALID_REQUEST ));
return res ;
}
int top_n = json_value ( body , "top_n" , ( int ) documents . size ());
// create and queue the task
json responses = json :: array ();
2025-12-22 14:23:34 +01:00
auto & rd = res -> rd ;
2025-11-29 22:04:44 +01:00
{
std :: vector < server_task > tasks ;
tasks . reserve ( documents . size ());
for ( size_t i = 0 ; i < documents . size (); i ++ ) {
auto tmp = format_prompt_rerank ( ctx_server . model , ctx_server . vocab , ctx_server . mctx , query , documents [ i ]);
server_task task = server_task ( SERVER_TASK_TYPE_RERANK );
2025-12-22 14:23:34 +01:00
task . id = rd . get_new_id ();
2025-11-29 22:04:44 +01:00
task . tokens = std :: move ( tmp );
tasks . push_back ( std :: move ( task ));
}
rd . post_tasks ( std :: move ( tasks ));
}
// wait for the results
auto all_results = rd . wait_for_all ( req . should_stop );
// collect results
if ( all_results . is_terminated ) {
return res ; // connection is closed
} else if ( all_results . error ) {
res -> error ( all_results . error -> to_json ());
return res ;
} else {
for ( auto & res : all_results . results ) {
GGML_ASSERT ( dynamic_cast < server_task_result_rerank *> ( res . get ()) != nullptr );
responses . push_back ( res -> to_json ());
}
}
// write JSON response
json root = format_response_rerank (
body ,
2025-12-22 14:23:34 +01:00
meta -> model_name ,
2025-11-29 22:04:44 +01:00
responses ,
is_tei_format ,
documents ,
top_n );
res -> ok ( root );
return res ;
};
2025-12-22 14:23:34 +01:00
this -> get_lora_adapters = [ this ]( const server_http_req & req ) {
auto res = create_response ();
auto & rd = res -> rd ;
{
server_task task ( SERVER_TASK_TYPE_GET_LORA );
task . id = rd . get_new_id ();
rd . post_task ( std :: move ( task ));
2025-11-29 22:04:44 +01:00
}
2025-12-22 14:23:34 +01:00
// get the result
2025-12-29 15:34:41 +02:00
auto result = rd . next ( req . should_stop );
if ( ! result ) {
// connection was closed
GGML_ASSERT ( req . should_stop ());
return res ;
}
2025-12-22 14:23:34 +01:00
if ( result -> is_error ()) {
res -> error ( result -> to_json ());
return res ;
}
GGML_ASSERT ( dynamic_cast < server_task_result_get_lora *> ( result . get ()) != nullptr );
res -> ok ( result -> to_json ());
2025-11-29 22:04:44 +01:00
return res ;
};
this -> post_lora_adapters = [ this ]( const server_http_req & req ) {
2025-12-22 14:23:34 +01:00
auto res = create_response ();
2025-11-29 22:04:44 +01:00
const json body = json :: parse ( req . body );
if ( ! body . is_array ()) {
res -> error ( format_error_response ( "Request body must be an array" , ERROR_TYPE_INVALID_REQUEST ));
return res ;
}
2025-12-22 14:23:34 +01:00
auto & rd = res -> rd ;
2025-11-29 22:04:44 +01:00
{
server_task task ( SERVER_TASK_TYPE_SET_LORA );
2025-12-22 14:23:34 +01:00
task . id = rd . get_new_id ();
task . set_lora = parse_lora_request ( body );
rd . post_task ( std :: move ( task ));
2025-11-29 22:04:44 +01:00
}
// get the result
2025-12-29 15:34:41 +02:00
auto result = rd . next ( req . should_stop );
if ( ! result ) {
// connection was closed
GGML_ASSERT ( req . should_stop ());
return res ;
}
2025-11-29 22:04:44 +01:00
if ( result -> is_error ()) {
res -> error ( result -> to_json ());
return res ;
}
GGML_ASSERT ( dynamic_cast < server_task_result_apply_lora *> ( result . get ()) != nullptr );
res -> ok ( result -> to_json ());
return res ;
};
}
std :: unique_ptr < server_res_generator > server_routes :: handle_slots_save ( const server_http_req & req , int id_slot ) {
2025-12-22 14:23:34 +01:00
auto res = create_response ();
2025-11-29 22:04:44 +01:00
const json request_data = json :: parse ( req . body );
std :: string filename = request_data . at ( "filename" );
if ( ! fs_validate_filename ( filename )) {
res -> error ( format_error_response ( "Invalid filename" , ERROR_TYPE_INVALID_REQUEST ));
return res ;
}
std :: string filepath = params . slot_save_path + filename ;
2025-12-22 14:23:34 +01:00
auto & rd = res -> rd ;
2025-11-29 22:04:44 +01:00
{
server_task task ( SERVER_TASK_TYPE_SLOT_SAVE );
2025-12-22 14:23:34 +01:00
task . id = rd . get_new_id ();
2026-01-30 19:46:10 +02:00
task . slot_action . id_slot = id_slot ;
2025-11-29 22:04:44 +01:00
task . slot_action . filename = filename ;
task . slot_action . filepath = filepath ;
2025-12-22 14:23:34 +01:00
rd . post_task ( std :: move ( task ));
2025-11-29 22:04:44 +01:00
}
2025-12-29 15:34:41 +02:00
auto result = rd . next ( req . should_stop );
if ( ! result ) {
// connection was closed
GGML_ASSERT ( req . should_stop ());
return res ;
}
2025-11-29 22:04:44 +01:00
if ( result -> is_error ()) {
res -> error ( result -> to_json ());
return res ;
}
res -> ok ( result -> to_json ());
return res ;
}
std :: unique_ptr < server_res_generator > server_routes :: handle_slots_restore ( const server_http_req & req , int id_slot ) {
2025-12-22 14:23:34 +01:00
auto res = create_response ();
2025-11-29 22:04:44 +01:00
const json request_data = json :: parse ( req . body );
std :: string filename = request_data . at ( "filename" );
if ( ! fs_validate_filename ( filename )) {
res -> error ( format_error_response ( "Invalid filename" , ERROR_TYPE_INVALID_REQUEST ));
return res ;
}
std :: string filepath = params . slot_save_path + filename ;
2025-12-22 14:23:34 +01:00
auto & rd = res -> rd ;
2025-11-29 22:04:44 +01:00
{
server_task task ( SERVER_TASK_TYPE_SLOT_RESTORE );
2025-12-22 14:23:34 +01:00
task . id = rd . get_new_id ();
2026-01-30 19:46:10 +02:00
task . slot_action . id_slot = id_slot ;
2025-11-29 22:04:44 +01:00
task . slot_action . filename = filename ;
task . slot_action . filepath = filepath ;
2025-12-22 14:23:34 +01:00
rd . post_task ( std :: move ( task ));
2025-11-29 22:04:44 +01:00
}
2025-12-29 15:34:41 +02:00
auto result = rd . next ( req . should_stop );
if ( ! result ) {
// connection was closed
GGML_ASSERT ( req . should_stop ());
return res ;
}
2025-11-29 22:04:44 +01:00
if ( result -> is_error ()) {
res -> error ( result -> to_json ());
return res ;
}
GGML_ASSERT ( dynamic_cast < server_task_result_slot_save_load *> ( result . get ()) != nullptr );
res -> ok ( result -> to_json ());
return res ;
}
2025-12-22 14:23:34 +01:00
std :: unique_ptr < server_res_generator > server_routes :: handle_slots_erase ( const server_http_req & req , int id_slot ) {
auto res = create_response ();
auto & rd = res -> rd ;
2025-11-29 22:04:44 +01:00
{
server_task task ( SERVER_TASK_TYPE_SLOT_ERASE );
2025-12-22 14:23:34 +01:00
task . id = rd . get_new_id ();
2026-01-30 19:46:10 +02:00
task . slot_action . id_slot = id_slot ;
2025-12-22 14:23:34 +01:00
rd . post_task ( std :: move ( task ));
2025-11-29 22:04:44 +01:00
}
2025-12-29 15:34:41 +02:00
auto result = rd . next ( req . should_stop );
if ( ! result ) {
// connection was closed
GGML_ASSERT ( req . should_stop ());
return res ;
}
2025-11-29 22:04:44 +01:00
if ( result -> is_error ()) {
res -> error ( result -> to_json ());
return res ;
}
GGML_ASSERT ( dynamic_cast < server_task_result_slot_erase *> ( result . get ()) != nullptr );
res -> ok ( result -> to_json ());
return res ;
}
std :: unique_ptr < server_res_generator > server_routes :: handle_embeddings_impl ( const server_http_req & req , task_response_type res_type ) {
2025-12-22 14:23:34 +01:00
auto res = create_response ();
if ( ! params . embedding ) {
2025-11-29 22:04:44 +01:00
res -> error ( format_error_response ( "This server does not support embeddings. Start it with `--embeddings`" , ERROR_TYPE_NOT_SUPPORTED ));
return res ;
}
2025-12-22 14:23:34 +01:00
if ( res_type != TASK_RESPONSE_TYPE_NONE && meta -> pooling_type == LLAMA_POOLING_TYPE_NONE ) {
2025-11-29 22:04:44 +01:00
res -> error ( format_error_response ( "Pooling type 'none' is not OAI compatible. Please use a different pooling type" , ERROR_TYPE_INVALID_REQUEST ));
return res ;
}
const json body = json :: parse ( req . body );
// for the shape of input/content, see tokenize_input_prompts()
json prompt ;
if ( body . count ( "input" ) != 0 ) {
prompt = body . at ( "input" );
} else if ( body . contains ( "content" )) {
res_type = TASK_RESPONSE_TYPE_NONE ; // "content" field is not OAI compatible
prompt = body . at ( "content" );
} else {
res -> error ( format_error_response ( " \" input \" or \" content \" must be provided" , ERROR_TYPE_INVALID_REQUEST ));
return res ;
}
bool use_base64 = false ;
if ( body . count ( "encoding_format" ) != 0 ) {
2025-12-22 14:23:34 +01:00
const std :: string & format = body . at ( "encoding_format" );
2025-11-29 22:04:44 +01:00
if ( format == "base64" ) {
use_base64 = true ;
} else if ( format != "float" ) {
res -> error ( format_error_response ( "The format to return the embeddings in. Can be either float or base64" , ERROR_TYPE_INVALID_REQUEST ));
return res ;
}
}
auto tokenized_prompts = tokenize_input_prompts ( ctx_server . vocab , ctx_server . mctx , prompt , true , true );
for ( const auto & tokens : tokenized_prompts ) {
// this check is necessary for models that do not add BOS token to the input
if ( tokens . empty ()) {
res -> error ( format_error_response ( "Input content cannot be empty" , ERROR_TYPE_INVALID_REQUEST ));
return res ;
}
}
int embd_normalize = 2 ; // default to Euclidean/L2 norm
if ( body . count ( "embd_normalize" ) != 0 ) {
embd_normalize = body . at ( "embd_normalize" );
2025-12-22 14:23:34 +01:00
if ( meta -> pooling_type == LLAMA_POOLING_TYPE_NONE ) {
SRV_DBG ( "embd_normalize is not supported by pooling type %d, ignoring it \n " , meta -> pooling_type );
2025-11-29 22:04:44 +01:00
}
}
// create and queue the task
json responses = json :: array ();
2025-12-22 14:23:34 +01:00
auto & rd = res -> rd ;
2025-11-29 22:04:44 +01:00
{
std :: vector < server_task > tasks ;
for ( size_t i = 0 ; i < tokenized_prompts . size (); i ++ ) {
server_task task = server_task ( SERVER_TASK_TYPE_EMBEDDING );
2025-12-22 14:23:34 +01:00
task . id = rd . get_new_id ();
2025-11-29 22:04:44 +01:00
task . tokens = std :: move ( tokenized_prompts [ i ]);
// OAI-compat
task . params . res_type = res_type ;
task . params . embd_normalize = embd_normalize ;
tasks . push_back ( std :: move ( task ));
}
rd . post_tasks ( std :: move ( tasks ));
}
// wait for the results
auto all_results = rd . wait_for_all ( req . should_stop );
// collect results
if ( all_results . is_terminated ) {
return res ; // connection is closed
} else if ( all_results . error ) {
res -> error ( all_results . error -> to_json ());
return res ;
} else {
for ( auto & res : all_results . results ) {
GGML_ASSERT ( dynamic_cast < server_task_result_embd *> ( res . get ()) != nullptr );
responses . push_back ( res -> to_json ());
}
}
// write JSON response
json root = res_type == TASK_RESPONSE_TYPE_OAI_EMBD
2025-12-22 14:23:34 +01:00
? format_embeddings_response_oaicompat ( body , meta -> model_name , responses , use_base64 )
2025-11-29 22:04:44 +01:00
: json ( responses );
res -> ok ( root );
return res ;
}