Skip to content

Commit 12e54ae

Browse files
65a65a
authored andcommitted
server : Support multimodal completion and embeddings prompts in JSON format
- Use server_tokens in more places in server and util.cpp - Convert most functions that used llama_tokens to server_tokens - Modify input tokenizer to handle JSON objects as subprompts - Break out MTMD prompt parsing into utility function - Support JSON objects with multimodal_data arrays for MTMD prompts along with other existing types - Add capability to model endpoint to indicate if client can send multimodal data - Add tests
1 parent 4227c9b commit 12e54ae

File tree

5 files changed

+324
-138
lines changed

5 files changed

+324
-138
lines changed

tools/server/README.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,10 @@ services:
226226
### Multimodal support
227227
228228
Multimodal support was added in [#12898](https://github.com/ggml-org/llama.cpp/pull/12898) and is currently an experimental feature.
229+
It is currently available in the following endpoints:
230+
- The OAI-compatible chat endpoint.
231+
- The non-OAI-compatible completions endpoint.
232+
- The non-OAI-compatible embeddings endpoint.
229233
230234
For more details, please refer to [multimodal documentation](../../docs/multimodal.md)
231235
@@ -400,12 +404,15 @@ These input shapes and data type are allowed for `prompt`:
400404
- Single string: `"string"`
401405
- Single sequence of tokens: `[12, 34, 56]`
402406
- Mixed tokens and strings: `[12, 34, "string", 56, 78]`
407+
- A JSON object which optionally contains multimodal data: `{ "prompt_string": "string", "multimodal_data": ["base64"] }`
403408

404409
Multiple prompts are also supported. In this case, the completion result will be an array.
405410

406411
- Only strings: `["string1", "string2"]`
407-
- Strings and sequences of tokens: `["string1", [12, 34, 56]]`
408-
- Mixed types: `[[12, 34, "string", 56, 78], [12, 34, 56], "string"]`
412+
- Strings, JSON objects, and sequences of tokens: `["string1", [12, 34, 56], { "prompt_string": "string", "multimodal_data": ["base64"]}]`
413+
- Mixed types: `[[12, 34, "string", 56, 78], [12, 34, 56], "string", { "prompt_string": "string" }]`
414+
415+
Note for `multimodal_data` in JSON object prompts. This should be an array of strings, containing base64 encoded multimodal data such as images and audio. There must be an identical number of MTMD media markers in the string prompt element which act as placeholders for the data provided to this parameter. The multimodal data files will be substituted in order. The marker string (e.g. `<__media__>`) can be found by calling `mtmd_default_marker()` defined in [the MTMD C API](https://github.com/ggml-org/llama.cpp/blob/5fd160bbd9d70b94b5b11b0001fd7f477005e4a0/tools/mtmd/mtmd.h#L87). A client *must not* specify this field unless the server has the multimodal capability. Clients should check `/models` or `/v1/models` for the `multimodal` capability before a multimodal request.
409416

410417
`temperature`: Adjust the randomness of the generated text. Default: `0.8`
411418

@@ -477,8 +484,6 @@ These words will not be included in the completion, so make sure to add them to
477484

478485
`t_max_predict_ms`: Set a time limit in milliseconds for the prediction (a.k.a. text-generation) phase. The timeout will trigger if the generation takes more than the specified time (measured since the first token was generated) and if a new-line character has already been generated. Useful for FIM applications. Default: `0`, which is disabled.
479486

480-
`image_data`: An array of objects to hold base64-encoded image `data` and its `id`s to be reference in `prompt`. You can determine the place of the image in the prompt as in the following: `USER:[img-12]Describe the image in detail.\nASSISTANT:`. In this case, `[img-12]` will be replaced by the embeddings of the image with id `12` in the following `image_data` array: `{..., "image_data": [{"data": "<BASE64_STRING>", "id": 12}]}`. Use `image_data` only with multimodal models, e.g., LLaVA.
481-
482487
`id_slot`: Assign the completion task to an specific slot. If is -1 the task will be assigned to a Idle slot. Default: `-1`
483488

484489
`cache_prompt`: Re-use KV cache from a previous request if possible. This way the common prefix does not have to be re-processed, only the suffix that differs between the requests. Because (depending on the backend) the logits are **not** guaranteed to be bit-for-bit identical for different batch sizes (prompt processing vs. token generation) enabling this option can cause nondeterministic results. Default: `true`
@@ -638,12 +643,12 @@ Returns a JSON object with a field `prompt` containing a string of the input mes
638643

639644
The same as [the embedding example](../embedding) does.
640645

646+
This endpoint also supports multimodal embeddings. See the documentation for the `/completions` endpoint for details on how to send a multimodal prompt.
647+
641648
*Options:*
642649

643650
`content`: Set the text to process.
644651

645-
`image_data`: An array of objects to hold base64-encoded image `data` and its `id`s to be reference in `content`. You can determine the place of the image in the content as in the following: `Image: [img-21].\nCaption: This is a picture of a house`. In this case, `[img-21]` will be replaced by the embeddings of the image with id `21` in the following `image_data` array: `{..., "image_data": [{"data": "<BASE64_STRING>", "id": 21}]}`. Use `image_data` only with multimodal models, e.g., LLaVA.
646-
647652
`embd_normalize`: Normalization for pooled embeddings. Can be one of the following values:
648653
```
649654
-1: No normalization

tools/server/server.cpp

Lines changed: 20 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4279,6 +4279,7 @@ int main(int argc, char ** argv) {
42794279
};
42804280

42814281
const auto handle_api_show = [&ctx_server, &res_ok](const httplib::Request &, httplib::Response & res) {
4282+
bool has_mtmd = ctx_server.mctx != nullptr;
42824283
json data = {
42834284
{
42844285
"template", common_chat_templates_source(ctx_server.chat_templates.get()),
@@ -4300,7 +4301,7 @@ int main(int argc, char ** argv) {
43004301
{"quantization_level", ""}
43014302
}},
43024303
{"model_info", ""},
4303-
{"capabilities", {"completion"}}
4304+
{"capabilities", has_mtmd ? json({"completion","multimodal"}) : json({"completion"})}
43044305
};
43054306

43064307
res_ok(res, data);
@@ -4326,56 +4327,15 @@ int main(int argc, char ** argv) {
43264327
// TODO: this log can become very long, put it behind a flag or think about a more compact format
43274328
//SRV_DBG("Prompt: %s\n", prompt.is_string() ? prompt.get<std::string>().c_str() : prompt.dump(2).c_str());
43284329

4329-
// process files
4330-
mtmd::bitmaps bitmaps;
4331-
const bool has_mtmd = ctx_server.mctx != nullptr;
4332-
{
4333-
if (!has_mtmd && !files.empty()) {
4334-
throw std::runtime_error("This server does not support multimodal");
4335-
}
4336-
for (auto & file : files) {
4337-
mtmd::bitmap bmp(mtmd_helper_bitmap_init_from_buf(ctx_server.mctx, file.data(), file.size()));
4338-
if (!bmp.ptr) {
4339-
throw std::runtime_error("Failed to load image or audio file");
4340-
}
4341-
// calculate bitmap hash (for KV caching)
4342-
std::string hash = fnv_hash(bmp.data(), bmp.n_bytes());
4343-
bmp.set_id(hash.c_str());
4344-
bitmaps.entries.push_back(std::move(bmp));
4345-
}
4346-
}
4347-
43484330
// process prompt
43494331
std::vector<server_tokens> inputs;
43504332

4351-
if (oaicompat && has_mtmd) {
4352-
// multimodal
4353-
std::string prompt_str = prompt.get<std::string>();
4354-
mtmd_input_text inp_txt = {
4355-
prompt_str.c_str(),
4356-
/* add_special */ true,
4357-
/* parse_special */ true,
4358-
};
4359-
mtmd::input_chunks chunks(mtmd_input_chunks_init());
4360-
auto bitmaps_c_ptr = bitmaps.c_ptr();
4361-
int32_t tokenized = mtmd_tokenize(ctx_server.mctx,
4362-
chunks.ptr.get(),
4363-
&inp_txt,
4364-
bitmaps_c_ptr.data(),
4365-
bitmaps_c_ptr.size());
4366-
if (tokenized != 0) {
4367-
throw std::runtime_error("Failed to tokenize prompt");
4368-
}
4369-
4370-
server_tokens tmp(chunks, true);
4371-
inputs.push_back(std::move(tmp));
4333+
if (oaicompat && ctx_server.mctx != nullptr) {
4334+
// This is the case used by OAI compatible chat path with MTMD. TODO It can be moved to the path below.
4335+
inputs.push_back(process_mtmd_prompt(ctx_server.mctx, prompt.get<std::string>(), files));
43724336
} else {
4373-
// non-multimodal version
4374-
auto tokenized_prompts = tokenize_input_prompts(ctx_server.vocab, prompt, true, true);
4375-
for (auto & p : tokenized_prompts) {
4376-
auto tmp = server_tokens(p, ctx_server.mctx != nullptr);
4377-
inputs.push_back(std::move(tmp));
4378-
}
4337+
// Everything else, including multimodal completions.
4338+
inputs = tokenize_input_prompts(ctx_server.vocab, ctx_server.mctx, prompt, true, true);
43794339
}
43804340

43814341
tasks.reserve(inputs.size());
@@ -4544,7 +4504,7 @@ int main(int argc, char ** argv) {
45444504
data["input_extra"] = input_extra; // default to empty array if it's not exist
45454505

45464506
std::string prompt = json_value(data, "prompt", std::string());
4547-
std::vector<llama_tokens> tokenized_prompts = tokenize_input_prompts(ctx_server.vocab, prompt, false, true);
4507+
std::vector<server_tokens> tokenized_prompts = tokenize_input_prompts(ctx_server.vocab, ctx_server.mctx, prompt, false, true);
45484508
SRV_DBG("creating infill tasks, n_prompts = %d\n", (int) tokenized_prompts.size());
45494509
data["prompt"] = format_infill(
45504510
ctx_server.vocab,
@@ -4555,7 +4515,7 @@ int main(int argc, char ** argv) {
45554515
ctx_server.params_base.n_predict,
45564516
ctx_server.slots[0].n_ctx, // TODO: there should be a better way
45574517
ctx_server.params_base.spm_infill,
4558-
tokenized_prompts[0]
4518+
tokenized_prompts[0].get_text_tokens() // TODO: this could maybe be multimodal.
45594519
);
45604520

45614521
std::vector<raw_buffer> files; // dummy
@@ -4604,7 +4564,7 @@ int main(int argc, char ** argv) {
46044564
if (current_state == SERVER_STATE_READY) {
46054565
model_meta = ctx_server.model_meta();
46064566
}
4607-
4567+
bool has_mtmd = ctx_server.mctx != nullptr;
46084568
json models = {
46094569
{"models", {
46104570
{
@@ -4616,7 +4576,7 @@ int main(int argc, char ** argv) {
46164576
{"type", "model"},
46174577
{"description", ""},
46184578
{"tags", {""}},
4619-
{"capabilities", {"completion"}},
4579+
{"capabilities", has_mtmd ? json({"completion","multimodal"}) : json({"completion"})},
46204580
{"parameters", ""},
46214581
{"details", {
46224582
{"parent_model", ""},
@@ -4733,7 +4693,7 @@ int main(int argc, char ** argv) {
47334693
}
47344694
}
47354695

4736-
auto tokenized_prompts = tokenize_input_prompts(ctx_server.vocab, prompt, true, true);
4696+
auto tokenized_prompts = tokenize_input_prompts(ctx_server.vocab, ctx_server.mctx, prompt, true, true);
47374697
for (const auto & tokens : tokenized_prompts) {
47384698
// this check is necessary for models that do not add BOS token to the input
47394699
if (tokens.empty()) {
@@ -4761,7 +4721,7 @@ int main(int argc, char ** argv) {
47614721

47624722
task.id = ctx_server.queue_tasks.get_new_id();
47634723
task.index = i;
4764-
task.prompt_tokens = server_tokens(tokenized_prompts[i], ctx_server.mctx != nullptr);
4724+
task.prompt_tokens = std::move(tokenized_prompts[i]);
47654725

47664726
// OAI-compat
47674727
task.params.oaicompat = oaicompat;
@@ -4848,22 +4808,25 @@ int main(int argc, char ** argv) {
48484808
return;
48494809
}
48504810

4851-
llama_tokens tokenized_query = tokenize_input_prompts(ctx_server.vocab, query, /* add_special */ false, true)[0];
4811+
std::vector<server_tokens> tokenized_queries = tokenize_input_prompts(ctx_server.vocab, ctx_server.mctx, query, /* add_special */ false, true);
4812+
if (tokenized_queries.size() != 1) {
4813+
res_error(res, format_error_response("\"query\" must contain only a single prompt", ERROR_TYPE_INVALID_REQUEST));
4814+
}
48524815

48534816
// create and queue the task
48544817
json responses = json::array();
48554818
bool error = false;
48564819
std::unordered_set<int> task_ids;
48574820
{
48584821
std::vector<server_task> tasks;
4859-
auto tokenized_docs = tokenize_input_prompts(ctx_server.vocab, documents, /* add_special */ false, true);
4822+
auto tokenized_docs = tokenize_input_prompts(ctx_server.vocab, ctx_server.mctx, documents, /* add_special */ false, true);
48604823
tasks.reserve(tokenized_docs.size());
48614824
for (size_t i = 0; i < tokenized_docs.size(); i++) {
4862-
auto tmp = format_rerank(ctx_server.vocab, tokenized_query, tokenized_docs[i]);
4825+
auto tmp = format_rerank(ctx_server.vocab, tokenized_queries[0], tokenized_docs[i]);
48634826
server_task task = server_task(SERVER_TASK_TYPE_RERANK);
48644827
task.id = ctx_server.queue_tasks.get_new_id();
48654828
task.index = i;
4866-
task.prompt_tokens = server_tokens(tmp, ctx_server.mctx != nullptr);
4829+
task.prompt_tokens = std::move(tmp);
48674830
tasks.push_back(std::move(task));
48684831
}
48694832

tools/server/tests/unit/test_completion.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
server = ServerPreset.tinyllama2()
88

9+
JSON_MULTIMODAL_KEY = "multimodal_data"
10+
JSON_PROMPT_STRING_KEY = "prompt_string"
911

1012
@pytest.fixture(scope="module", autouse=True)
1113
def create_server():
@@ -231,6 +233,28 @@ def test_nocache_long_input_prompt():
231233
})
232234
assert res.status_code == 200
233235

236+
def test_json_prompt_no_mtmd():
237+
global server
238+
server.start()
239+
res = server.make_request("POST", "/completion", data={
240+
"prompt": { JSON_PROMPT_STRING_KEY: "I believe the meaning of life is" },
241+
"seed": 42,
242+
"temperature": 1.0,
243+
"cache_prompt": False,
244+
})
245+
assert res.status_code == 200
246+
247+
def test_json_prompt_mtm_error_when_not_supported():
248+
global server
249+
server.start()
250+
res = server.make_request("POST", "/completion", data={
251+
"prompt": { JSON_PROMPT_STRING_KEY: "I believe the meaning of life is <__media__>", JSON_MULTIMODAL_KEY: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" },
252+
"seed": 42,
253+
"temperature": 1.0,
254+
"cache_prompt": False,
255+
})
256+
# MTMD is disabled on this model, so this should fail.
257+
assert res.status_code != 200
234258

235259
def test_completion_with_tokens_input():
236260
global server
@@ -269,6 +293,20 @@ def test_completion_with_tokens_input():
269293
assert len(res.body) == 2
270294
assert res.body[0]["content"] == res.body[1]["content"]
271295

296+
# mixed JSON and tokens
297+
res = server.make_request("POST", "/completion", data={
298+
"prompt": [
299+
tokens,
300+
{
301+
JSON_PROMPT_STRING_KEY: "I believe the meaning of life is",
302+
},
303+
],
304+
})
305+
assert res.status_code == 200
306+
assert type(res.body) == list
307+
assert len(res.body) == 2
308+
assert res.body[0]["content"] == res.body[1]["content"]
309+
272310
# mixed string and tokens in one sequence
273311
res = server.make_request("POST", "/completion", data={
274312
"prompt": [1, 2, 3, 4, 5, 6, prompt_str, 7, 8, 9, 10, prompt_str],

0 commit comments

Comments
 (0)