211 {
213 std::string
host =
"localhost";
216
219 }
222 }
225 }
228 }
232 }
233
236 Logger::info(
"EMBER_DETERMINISTIC set: deterministic CUDA mode (run-to-run bit-identical logits; /health "
237 "reports \"deterministic\": true).");
238
239 std::shared_ptr<tinyllama::TinyLlamaSession>
session;
240 try {
244 }
catch (
const std::exception&
e) {
246 return 1;
247 }
248
250
251
252
253
255 const char*
v = std::getenv(
"EMBER_PUBLIC");
256 return v && std::string(
v) ==
"1";
257 }();
260 svr.set_payload_max_length(64 * 1024);
261 svr.set_read_timeout(10, 0);
262 svr.set_write_timeout(30, 0);
263 }
264
265
266
267 if (
const char*
scripted = std::getenv(
"EMBER_TEST_SCRIPTED_REPLIES")) {
269 Logger::error(
"EMBER_TEST_SCRIPTED_REPLIES is a test hook: refused under EMBER_PUBLIC=1");
270 return 1;
271 }
273 const json s = json::parse(
f,
nullptr,
false);
274 if (!
session->is_bpe() ||
s.is_discarded() || !
s.is_array()) {
276 " is not a JSON list of id lists, or the model has no chat mode");
277 return 1;
278 }
279 session->set_scripted_replies(
s.get<std::vector<std::vector<int>>>());
280 Logger::warning(
"EMBER_TEST_SCRIPTED_REPLIES: " + std::to_string(
s.size()) +
281 " scripted replies stand in for sampling (a test server)");
282 }
283
284 if (std::filesystem::exists(
www_path) &&
285 std::filesystem::is_directory(
www_path)) {
290 return 1;
291 }
292 } else {
294 ". Web client will not be served.");
295 }
296
297
298
299
300
301
303
304 static std::atomic<int>
waiting{0};
308 };
310 httplib::Response&
res) {
311
312 const auto t_request = std::chrono::steady_clock::now();
313
317 res.set_header(
"Access-Control-Allow-Origin",
"*");
318 res.set_header(
"Retry-After",
"2");
319 res.set_content(
json_out(
json{{
"error",
"busy: too many people chatting at once, try again in a moment"}}),
320 "application/json");
321 return;
322 }
325 std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() -
t_request).count();
327 res.set_header(
"Access-Control-Allow-Origin",
"*");
328 res.set_header(
"Access-Control-Allow-Methods",
"POST, OPTIONS");
329 res.set_header(
"Access-Control-Allow-Headers",
"Content-Type");
330
332
333 float temperature = 0.1f;
334 int max_new_tokens = 60;
335 int top_k = 40;
336 float top_p = 0.9f;
337
338
339
342 try {
344 if (
req_json.contains(
"user_input")) {
346 } else if (!tool_turns) {
347 throw std::runtime_error("Missing 'user_input' field in request JSON");
348 }
349
350 if (
req_json.contains(
"max_new_tokens"))
351 max_new_tokens =
req_json[
"max_new_tokens"].get<
int>();
352 if (
req_json.contains(
"temperature"))
358
363 res.set_header(
"Access-Control-Allow-Origin",
"*");
364 res.set_content(
json_out(
json{{
"error",
"message too long (600 characters at most)"}}),
"application/json");
365 return;
366 }
367 max_new_tokens = std::min(std::max(max_new_tokens, 1), 64);
368 temperature = std::min(std::max(temperature, 0.0f), 1.5f);
369 top_k = std::min(std::max(top_k, 0), 200);
370 top_p = std::min(std::max(top_p, 0.05f), 1.0f);
377
381
384 hist[
at].value(
"role",
"") !=
"user")
385 hist.erase(
hist.begin() +
static_cast<std::ptrdiff_t
>(
at));
387 for (
const char* key : {
"content",
"user",
"assistant"})
388 if (
h.is_object() &&
h.contains(key) &&
h[key].is_string() &&
389 h[key].get<std::string>().size() >
kMaxText)
391 }
392 }
393
394
395
396
397
398
399
402 try {
408 return;
409 }
410 r.max_new_tokens = max_new_tokens;
411 r.temperature = temperature;
415 r.seed =
r.has_seed ?
req_json[
"seed"].get<
unsigned>() : 0
u;
416 r.want_ids =
req_json.value(
"return_ids",
false);
422 return;
423 }
426 try {
431 return;
432 }
433 if (!
bp.error.empty()) {
436 return;
437 }
438 if (!
req_json.value(
"stream",
false)) {
440 return;
441 }
442
443
444
445
446
447 res.set_header(
"Cache-Control",
"no-cache");
448 res.set_chunked_content_provider(
452 const double waited = std::chrono::duration<double, std::milli>(
453 std::chrono::steady_clock::now() -
t_request).count();
455 const std::string
ev =
"data: " +
json_out(
j) +
"\n\n";
457 };
463 return true;
464 });
465 return;
466 }
467
470
474
476
477 if (
config.is_gguf_file_loaded) {
479
480
481
482
483
484
491 Logger::info(std::string(
"WORD_LEVEL model type probed: ") +
493 }
497 lower.find(
"summary:") != std::string::npos ||
498 lower.find(
"words:") != std::string::npos ||
499 lower.rfind(
"once upon", 0) == 0 ||
500 lower.rfind(
"user:", 0) == 0;
503
504
510 Logger::info(
"WORD_LEVEL: wrapped input as dialogue turn.");
511 } else {
514 Logger::info(
"WORD_LEVEL: wrapped free-form input as story request.");
515 }
516 }
517 }
518
521 Logger::info(
"GGUF Llama 3 model detected (via tokenizer_family). Q&A prompt formatting will be DISABLED for session generate.");
522 } else {
525 "GGUF (Non-Llama 3) model detected. Using Q:A: format via session->generate.");
526 }
527 } else {
533 "Safetensors model detected. Applied chat template via "
534 "tokenizer. Prompt: " +
536 } else {
538 "CRITICAL: Tokenizer not available for Safetensors model in "
539 "server. Cannot apply chat template.");
540
542 }
544 }
545
549
550
551 {
553 if (cut != std::string::npos && cut > 0) {
555 }
556 }
558
563
564
565
567 auto ms = [](
auto a,
auto b) {
568 return std::chrono::duration<double, std::milli>(
b -
a).count();
569 };
573 {"request_to_first_token_ms",
576 {
"ttft_ms",
tb.ttft_ms},
577 {
"num_prompt_tokens",
tb.num_prompt_tokens},
578 {
"batch_prefill",
tb.batch_prefill},
579 {
"tokenize_ms",
tb.tokenize_ms},
580 {
"kv_reset_ms",
tb.kv_reset_ms},
581 {
"embed_ms",
tb.embed_ms},
582 {
"prefill_wall_ms",
tb.prefill_wall_ms},
583 {
"prefill_gpu_span_ms",
tb.prefill_gpu_span_ms},
584 {
"sample_ms",
tb.sample_ms},
585 {
"prefix_prev_cached_tokens",
tb.prefix_prev_cached_tokens},
586 {
"prefix_reusable_tokens",
tb.prefix_reusable_tokens},
587 {
"prefix_reused_tokens",
tb.prefix_reused_tokens}, {
"spec_drafted",
tb.spec_drafted}, {
"spec_accepted",
tb.spec_accepted}, {
"spec_passes",
tb.spec_passes},
588 {
"prefix_hit_rate",
tb.prefix_hit_rate},
589 {
"op_ms",
tb.op_ms}};
590 if (
tb.spec_draft_calls > 0) {
591 res_json[
"ttft"][
"spec_draft_ms"] =
tb.spec_draft_ms;
592 res_json[
"ttft"][
"spec_draft_calls"] =
tb.spec_draft_calls;
593 }
594
597
598 }
catch (
const json::parse_error&
e) {
602 err_json[
"error"] =
"Invalid JSON format: " + std::string(
e.what());
604 }
catch (
const std::exception&
e) {
608 err_json[
"error"] =
"Internal server error: " + std::string(
e.what());
610 }
611 });
612
613
619 h[
"tokenizer"] =
session->is_bpe() ?
"bpe" :
"ember";
620
621
623 h[
"chat_template"] = {{
"name",
t->name}, {
"version", json::parse(
t->version.dump())}};
624 else
625 h[
"chat_template"] =
nullptr;
626 h[
"chat_template_why"] =
session->chat_template_why();
628 h[
"vocab_size"] =
c.vocab_size;
629 h[
"hidden_size"] =
c.hidden_size;
630 h[
"layers"] =
c.num_hidden_layers;
631
633 res.set_header(
"Access-Control-Allow-Origin",
"*");
635 });
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650 const std::string
model_id = std::filesystem::path(
model_dir).filename().string();
653 res.set_header(
"Access-Control-Allow-Origin",
"*");
654 res.set_content(
json_out(
json{{
"error", {{
"message",
msg}, {
"type",
"invalid_request_error"}}}}),
655 "application/json");
656 };
657 svr.Get(
"/v1/models", [
model_id](
const httplib::Request&, httplib::Response&
res) {
658 res.set_header(
"Access-Control-Allow-Origin",
"*");
661 {"object", "model"},
662 {"created", 0},
663 {"owned_by", "ember.cpp"}}})}}),
664 "application/json");
665 });
666 svr.Options(
"/v1/chat/completions", [](
const httplib::Request&, httplib::Response&
res) {
667 res.set_header(
"Access-Control-Allow-Origin",
"*");
668 res.set_header(
"Access-Control-Allow-Headers",
"Content-Type, Authorization");
669 res.set_header(
"Access-Control-Allow-Methods",
"POST, OPTIONS");
671 });
673 httplib::Response&
res) {
674 const auto t_request = std::chrono::steady_clock::now();
677 res.set_header(
"Retry-After",
"2");
678 openai_error(
res, 503,
"busy: too many requests at once, try again in a moment");
679 return;
680 }
682 openai_error(
res, 400,
"this model has no chat template ember can apply; use POST /chat");
683 return;
684 }
686 if (
body.is_discarded()) {
687 openai_error(
res, 400,
"body must be JSON with a non-empty 'messages' array");
688 return;
689 }
694 r.max_new_tokens = 64;
697 try {
698
699
700
701
703 try {
708 return;
709 }
710 if (
body.contains(
"temperature") &&
body[
"temperature"].is_number())
r.temperature =
body[
"temperature"].get<
float>();
711 if (
body.contains(
"top_p") &&
body[
"top_p"].is_number())
r.top_p =
body[
"top_p"].get<
float>();
712 if (
body.contains(
"top_k") &&
body[
"top_k"].is_number())
r.top_k =
body[
"top_k"].get<
int>();
713 for (const char* key : {"max_completion_tokens", "max_tokens"})
714 if (
body.contains(key) &&
body[key].is_number()) {
715 r.max_new_tokens =
body[key].get<
int>();
716 break;
717 }
718 if (
body.contains(
"seed") &&
body[
"seed"].is_number()) {
720 r.seed =
static_cast<unsigned>(
body[
"seed"].get<
long long>());
721 }
723 if (
body.contains(
"stream_options") &&
body[
"stream_options"].is_object())
725 }
catch (
const json::exception&
e) {
727 return;
728 }
729
730
731 for (const char* key : {"frequency_penalty", "presence_penalty"})
732 if (
body.contains(key) && !
body[key].is_null() && !(
body[key].is_number() &&
body[key].get<
double>() == 0.0)) {
733 openai_error(
res, 400, std::string(key) +
" is not supported (only 0); use repeat_penalty and repeat_last_n");
734 return;
735 }
736 {
741 return;
742 }
743 }
744 r.max_new_tokens = std::max(
r.max_new_tokens, 1);
746
748 std::vector<microtorch::chat::Message>&
m =
r.conv.messages;
749 if (
m.back().role ==
"user" &&
m.back().content.size() >
kMaxText) {
751 return;
752 }
753 r.max_new_tokens = std::min(
r.max_new_tokens, 64);
754 r.temperature = std::min(std::max(
r.temperature, 0.0f), 1.5f);
755 r.top_k = std::min(std::max(
r.top_k, 0), 200);
756 r.top_p = std::min(std::max(
r.top_p, 0.05f), 1.0f);
766 return;
767 }
768 r.max_new_tokens = std::min(
r.max_new_tokens, 64);
769 r.temperature = std::min(std::max(
r.temperature, 0.0f), 1.5f);
770 r.top_k = std::min(std::max(
r.top_k, 0), 200);
771 r.top_p = std::min(std::max(
r.top_p, 0.05f), 1.0f);
773 while (
r.conv.turns.size() > 8)
r.conv.turns.erase(
r.conv.turns.begin());
774 for (
auto&
t :
r.conv.turns) {
777 }
778 }
781 std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() -
t_request).count();
784 try {
788 return;
789 }
790 if (!
bp.error.empty()) {
792 return;
793 }
794 static std::mt19937_64
id_rng{std::random_device{}()};
796 std::snprintf(
idbuf,
sizeof idbuf,
"chatcmpl-%016llx",
static_cast<unsigned long long>(
id_rng()));
797 const std::string
id =
idbuf;
798 const long long created =
static_cast<long long>(std::time(
nullptr));
799 res.set_header(
"Access-Control-Allow-Origin",
"*");
802 return json{{
"prompt_tokens",
bp.prompt_tokens},
805 };
806
807
809 if (
result.value(
"stop_reason", std::string(
"eos")) ==
"length")
return "length";
810 return result.contains(
"tool_calls") ?
"tool_calls" :
"stop";
811 };
817 {"object", "chat.completion"},
820 {
"choices", json::array({
json{{
"index", 0},
822 {"logprobs", nullptr},
826 return;
827 }
828 res.set_header(
"Cache-Control",
"no-cache");
829 res.set_chunked_content_provider(
830 "text/event-stream",
832 size_t, httplib::DataSink&
sink) {
836 std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() -
t_request).count();
838 json c = {{
"id",
id},
839 {"object", "chat.completion.chunk"},
842 {
"choices", json::array({
json{{
"index", 0}, {
"delta",
delta}, {
"finish_reason", finish}}})}};
843 const std::string
ev =
"data: " +
json_out(
c) +
"\n\n";
845 };
846 chunk(
json{{
"role",
"assistant"}, {
"content",
""}},
nullptr);
849
850 if (
result.contains(
"tool_calls"))
851 for (
size_t k = 0;
k <
result[
"tool_calls"].size(); ++
k) {
855 }
858 json u = {{
"id",
id},
859 {"object", "chat.completion.chunk"},
862 {"choices", json::array()},
864 const std::string
ev =
"data: " +
json_out(
u) +
"\n\n";
866 }
867 const std::string
done =
"data: [DONE]\n\n";
870 return true;
871 });
872 });
873
874
875
876
877
878
879
880 svr.Post(
"/score", [&
session](
const httplib::Request&
req, httplib::Response&
res) {
881 const auto t_request = std::chrono::steady_clock::now();
883 res.set_header(
"Access-Control-Allow-Origin",
"*");
887 };
889 refuse(403,
"/score is off on a public server (EMBER_PUBLIC=1)");
890 return;
891 }
892 const json body = json::parse(
req.body,
nullptr,
false);
893 if (
body.is_discarded()) {
894 refuse(400,
"body must be JSON: {\"requests\": [{\"key\", \"prompt_ids\", \"continuation_ids\"}, ...]}");
895 return;
896 }
899 m.ctx =
static_cast<size_t>(
session->score_context());
902 m.state_k =
static_cast<size_t>(
session->state_k());
904 m.user =
f.turn_ids.at(0);
905 m.assistant =
f.turn_ids.at(1);
906 }
907 std::vector<ember::ScoreItem>
items;
911 return;
912 }
915 const auto t_start = std::chrono::steady_clock::now();
918 try {
922 it.rec[
"over_ctx"] =
true;
925 continue;
926 }
927 std::vector<int> ctx =
it.prompt;
928 ctx.insert(ctx.end(),
it.cont.begin(),
it.cont.end() - 1);
930 const std::vector<float>
rows =
938 }
939 }
catch (
const std::invalid_argument&
e) {
940 refuse(400, std::string(
"score: ") +
e.what());
941 return;
942 }
catch (
const std::exception&
e) {
943 refuse(500, std::string(
"score: ") +
e.what());
944 return;
945 }
946 auto ms = [](
auto a,
auto b) {
return std::chrono::duration<double, std::milli>(
b -
a).count(); };
947 const auto t_end = std::chrono::steady_clock::now();
949 {
"stats", {{
"requests",
items.size()},
957 });
958 svr.Options(
"/score", [](
const httplib::Request&, httplib::Response&
res) {
959 res.set_header(
"Access-Control-Allow-Origin",
"*");
960 res.set_header(
"Access-Control-Allow-Headers",
"Content-Type");
961 res.set_header(
"Access-Control-Allow-Methods",
"POST, OPTIONS");
963 });
964
965 svr.Options(
"/chat", [](
const httplib::Request&
req, httplib::Response&
res) {
966 res.set_header(
"Access-Control-Allow-Origin",
"*");
967 res.set_header(
"Access-Control-Allow-Headers",
"Content-Type");
968 res.set_header(
"Access-Control-Allow-Methods",
"POST, OPTIONS");
970 });
971
974 " with " + std::to_string(
num_threads) +
" threads.");
975
976
978
980 return 0;
981}
static void warning(const std::string &message)
static void set_level(Level new_level)
static void info(const std::string &message)
static void error(const std::string &message)
A lightweight tokenizer implementation for text processing.
std::vector< int > encode(const std::string &text, bool add_bos=true, bool add_eos=false, PreTokenizeMethod pre_tok_override=PreTokenizeMethod::DEFAULT) const
Encodes text into token IDs with optional special tokens.
Represents an active TinyLlama session holding the loaded model and tokenizer.
std::vector< Value > items
bool tool_turns(const chat::Template *t)
std::vector< Stage > lower(const Expr &e)
Conversation openai_conversation(const nlohmann::json &body, const chat::Template *t=nullptr, const chat::json &tools=chat::json())
std::string parse_score_body(const nlohmann::json &body, const ScoreModel &m, std::vector< ScoreItem > &out)
void score_fill(nlohmann::json &rec, const std::vector< int > &c, const float *rows, size_t V, const std::vector< char > &banned)
chat::json ordered_tools(const std::string &body)
Conversation chat_conversation(const nlohmann::json &r, const chat::Template *t=nullptr, const chat::json &tools=chat::json())
std::vector< char > score_banned(const std::vector< std::string > &tokens, size_t vocab)
std::vector< std::pair< size_t, size_t > > exchanges(const std::vector< Message > &m)
std::string render(const std::vector< Message > &m, const Template &t, bool generation_prompt, int thinking=-1, const std::vector< json > &tools={})
std::vector< Message > from_pairs(const std::vector< std::pair< std::string, std::string > > &turns, const std::string *user=nullptr, const std::string *system=nullptr)
static std::string json_out(const json &j)
static json bpe_run(tinyllama::TinyLlamaSession &sess, const BpeChatRequest &r, const BpePrompt &p, std::chrono::steady_clock::time_point t_request, double waited_ms, const std::function< void(const std::string &)> &on_piece)
static std::string bpe_repeat_fields(const json &body, BpeChatRequest &r, int ctx, bool clamp)
static BpePrompt bpe_build_prompt(tinyllama::TinyLlamaSession &sess, const BpeChatRequest &r)
Model configuration structure holding architecture and hyperparameters.
int max_position_embeddings