mtmd, server: add "placeholder bitmap" for counting tokens , add */input_tokens API (#23913)
* mtmd: add "placeholder bitmap" for counting tokens w/o preprocessing * fast path skip preproc for placeholder * fix build * correct the api * add server endpoint + tests * add object name * update docs * add proxy handling * fix build * fix audio input path * use is_placeholder in process_mtmd_prompt() * nits * nits (2) * docs: clarify chat/completions/input_tokens is not official * fix merge problem
This commit is contained in:
+152
-56
@@ -26,12 +26,46 @@
|
||||
|
||||
// represents raw image data, layout is RGBRGBRGB...
|
||||
// length of data must be nx * ny * 3
|
||||
// for audio bitmap: nx = sample count, ny = 1, layout is F32 F32 F32 ...
|
||||
// length of data must be nx * sizeof(float)
|
||||
struct mtmd_bitmap {
|
||||
uint32_t nx;
|
||||
uint32_t ny;
|
||||
std::vector<unsigned char> data;
|
||||
uint32_t nx = 0;
|
||||
uint32_t ny = 0;
|
||||
std::string id; // optional user-defined id, for ex: can be set to image hash, useful for KV cache tracking
|
||||
bool is_audio = false; // true if the bitmap is audio
|
||||
|
||||
mtmd_bitmap(const unsigned char * data, uint32_t nx, uint32_t ny)
|
||||
: nx(nx), ny(ny) {
|
||||
if (data) {
|
||||
size_t data_size = (size_t)nx * ny * 3;
|
||||
this->data.resize(data_size);
|
||||
std::memcpy(this->data.data(), data, data_size);
|
||||
}
|
||||
}
|
||||
|
||||
mtmd_bitmap(const unsigned char * data, uint32_t n_samples)
|
||||
: nx(n_samples), ny(1), is_audio(true) {
|
||||
if (data) {
|
||||
size_t data_size = (size_t)nx * sizeof(float);
|
||||
this->data.resize(data_size);
|
||||
std::memcpy(this->data.data(), data, data_size);
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<unsigned char> & get_ro_buf() const {
|
||||
return data;
|
||||
}
|
||||
|
||||
bool is_placeholder() const {
|
||||
return data.empty();
|
||||
}
|
||||
|
||||
size_t n_bytes() const {
|
||||
return data.size();
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<unsigned char> data;
|
||||
};
|
||||
|
||||
// position indexing for decoder model
|
||||
@@ -42,8 +76,8 @@ enum mtmd_pos_type {
|
||||
};
|
||||
|
||||
struct mtmd_image_tokens {
|
||||
uint32_t nx; // number of tokens in x direction
|
||||
uint32_t ny; // number of tokens in y direction
|
||||
uint32_t nx = 0; // number of tokens in x direction
|
||||
uint32_t ny = 0; // number of tokens in y direction
|
||||
mtmd_pos_type pos = MTMD_POS_TYPE_NORMAL;
|
||||
uint32_t image_idx = 0; // 0-based position of this image among image chunks in the prompt(used by pos == MTMD_POS_TYPE_HUNYUANVL)
|
||||
uint32_t n_tokens() const {
|
||||
@@ -56,6 +90,16 @@ struct mtmd_image_tokens {
|
||||
clip_image_f32_batch batch_f32; // preprocessed image patches
|
||||
std::string id; // optional user-defined ID, useful for KV cache tracking
|
||||
|
||||
// true if one of entries in batch_f32 is a placeholder
|
||||
bool is_placeholder() const {
|
||||
for (const auto & entry : batch_f32.entries) {
|
||||
if (entry->is_placeholder()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
mtmd_image_tokens clone() {
|
||||
return mtmd_image_tokens{
|
||||
nx,
|
||||
@@ -70,10 +114,20 @@ struct mtmd_image_tokens {
|
||||
using mtmd_image_tokens_ptr = std::unique_ptr<mtmd_image_tokens>;
|
||||
|
||||
struct mtmd_audio_tokens {
|
||||
uint32_t n_tokens; // number of tokens
|
||||
uint32_t n_tokens = 0; // number of tokens
|
||||
clip_image_f32_batch batch_f32; // preprocessed image patches
|
||||
std::string id; // optional user-defined ID, useful for KV cache tracking
|
||||
|
||||
// true if one of entries in batch_f32 is a placeholder
|
||||
bool is_placeholder() const {
|
||||
for (const auto & entry : batch_f32.entries) {
|
||||
if (entry->is_placeholder()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
mtmd_audio_tokens clone() {
|
||||
return mtmd_audio_tokens{
|
||||
n_tokens,
|
||||
@@ -795,16 +849,19 @@ struct mtmd_tokenizer {
|
||||
}
|
||||
|
||||
// sanity check
|
||||
GGML_ASSERT(bitmap->nx > 0 && bitmap->ny > 0);
|
||||
GGML_ASSERT(bitmap->data.size() == (size_t)bitmap->nx * bitmap->ny * 3);
|
||||
if (bitmap->nx <= 0 || bitmap->ny <= 0) {
|
||||
LOG_ERR("%s: error: invalid bitmap dimensions: nx = %d, ny = %d\n",
|
||||
__func__, bitmap->nx, bitmap->ny);
|
||||
return 2;
|
||||
}
|
||||
GGML_ASSERT(ctx->image_preproc != nullptr);
|
||||
|
||||
// convert mtmd_bitmap to clip_image_u8
|
||||
clip_image_u8_ptr img_u8(clip_image_u8_init());
|
||||
img_u8->nx = bitmap->nx;
|
||||
img_u8->ny = bitmap->ny;
|
||||
img_u8->buf.resize(bitmap->data.size());
|
||||
std::memcpy(img_u8->buf.data(), bitmap->data.data(), img_u8->nx * img_u8->ny * 3);
|
||||
img_u8->set_size(
|
||||
{(int)bitmap->nx, (int)bitmap->ny},
|
||||
bitmap->is_placeholder());
|
||||
img_u8->cpy_buf(bitmap->get_ro_buf());
|
||||
|
||||
// preprocess image
|
||||
clip_image_f32_batch batch_f32;
|
||||
@@ -949,7 +1006,7 @@ struct mtmd_tokenizer {
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (bitmap->data.size() == 0) {
|
||||
if (bitmap->nx == 0) {
|
||||
LOG_ERR("%s: error: empty audio data\n", __func__);
|
||||
return 2;
|
||||
}
|
||||
@@ -960,26 +1017,46 @@ struct mtmd_tokenizer {
|
||||
|
||||
// sanity check
|
||||
GGML_ASSERT(ctx->audio_preproc != nullptr);
|
||||
GGML_ASSERT(bitmap->data.size() > sizeof(float));
|
||||
GGML_ASSERT(bitmap->data.size() % sizeof(float) == 0);
|
||||
|
||||
// preprocess audio
|
||||
std::vector<mtmd_audio_mel> mel_spec_chunks;
|
||||
const float * samples = (const float *)bitmap->data.data();
|
||||
size_t n_samples = bitmap->data.size() / sizeof(float);
|
||||
bool ok = ctx->audio_preproc->preprocess(samples, n_samples, mel_spec_chunks);
|
||||
if (!ok) {
|
||||
LOG_ERR("Unable to preprocess audio\n");
|
||||
return 2;
|
||||
{
|
||||
std::vector<float> dummy;
|
||||
const float * samples = nullptr;
|
||||
size_t n_samples = 0;
|
||||
if (bitmap->is_placeholder()) {
|
||||
// TODO @ngxson : skip underlay processing if bitmap is placeholder
|
||||
GGML_ASSERT(bitmap->ny == 1);
|
||||
|
||||
dummy.resize(bitmap->nx);
|
||||
samples = dummy.data();
|
||||
n_samples = dummy.size();
|
||||
} else {
|
||||
const auto & buf = bitmap->get_ro_buf();
|
||||
GGML_ASSERT(buf.size() > sizeof(float));
|
||||
GGML_ASSERT(buf.size() % sizeof(float) == 0);
|
||||
|
||||
samples = (const float *)buf.data();
|
||||
n_samples = buf.size() / sizeof(float);
|
||||
}
|
||||
bool ok = ctx->audio_preproc->preprocess(samples, n_samples, mel_spec_chunks);
|
||||
if (!ok) {
|
||||
LOG_ERR("Unable to preprocess audio\n");
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
// consider each mel_spec as a separate audio chunk
|
||||
// TODO: maybe support batching, but this may come with memory cost
|
||||
for (auto & mel_spec : mel_spec_chunks) {
|
||||
const bool is_placeholder = mel_spec.data.empty();
|
||||
|
||||
clip_image_f32_ptr mel_f32(clip_image_f32_init());
|
||||
mel_f32->nx = mel_spec.n_len;
|
||||
mel_f32->ny = mel_spec.n_mel;
|
||||
mel_f32->buf = std::move(mel_spec.data);
|
||||
mel_f32->set_size(
|
||||
{mel_spec.n_len, mel_spec.n_mel},
|
||||
is_placeholder, /* is_audio */ true);
|
||||
mel_f32->cpy_buf(mel_spec.data);
|
||||
|
||||
size_t n_tokens = clip_n_output_tokens(ctx->ctx_a, mel_f32.get());
|
||||
|
||||
clip_image_f32_batch batch_f32;
|
||||
@@ -1098,12 +1175,28 @@ int32_t mtmd_encode_chunk(mtmd_context * ctx, const mtmd_input_chunk * chunk) {
|
||||
LOG_ERR("%s: model does not support vision input\n", __func__);
|
||||
return 1;
|
||||
}
|
||||
if (chunk->tokens_image == nullptr) {
|
||||
LOG_ERR("%s: image tokens are null\n", __func__);
|
||||
return 1;
|
||||
}
|
||||
if (chunk->tokens_image->is_placeholder()) {
|
||||
LOG_ERR("%s: image tokens batch is placeholder\n", __func__);
|
||||
return 1;
|
||||
}
|
||||
return mtmd_encode(ctx, chunk->tokens_image.get());
|
||||
} else if (chunk->type == MTMD_INPUT_CHUNK_TYPE_AUDIO) {
|
||||
if (!ctx->ctx_a) {
|
||||
LOG_ERR("%s: model does not support audio input\n", __func__);
|
||||
return 1;
|
||||
}
|
||||
if (chunk->tokens_audio == nullptr) {
|
||||
LOG_ERR("%s: audio tokens are null\n", __func__);
|
||||
return 1;
|
||||
}
|
||||
if (chunk->tokens_audio->is_placeholder()) {
|
||||
LOG_ERR("%s: audio tokens batch is placeholder\n", __func__);
|
||||
return 1;
|
||||
}
|
||||
int n_mmproj_embd = ctx->n_embd_text;
|
||||
ctx->image_embd_v.resize(chunk->tokens_audio->n_tokens * n_mmproj_embd);
|
||||
bool ok = clip_image_batch_encode(
|
||||
@@ -1141,6 +1234,10 @@ int32_t mtmd_encode(mtmd_context * ctx, const mtmd_image_tokens * image_tokens)
|
||||
// e.g., DeepSeek-OCR-2: 144 per tile views, 257 for the global view
|
||||
size_t offset = 0;
|
||||
for (size_t i = 0; i < entries.size(); i++) {
|
||||
if (entries[i]->is_placeholder()) {
|
||||
LOG_ERR("%s: image tokens batch entry %zu is placeholder\n", __func__, i);
|
||||
return 1;
|
||||
}
|
||||
int n_tokens_per_image = clip_n_output_tokens(ctx_clip, entries[i].get());
|
||||
ok = clip_image_encode(
|
||||
ctx_clip,
|
||||
@@ -1150,6 +1247,10 @@ int32_t mtmd_encode(mtmd_context * ctx, const mtmd_image_tokens * image_tokens)
|
||||
offset += static_cast<size_t>(n_mmproj_embd) * n_tokens_per_image;
|
||||
}
|
||||
} else {
|
||||
if (image_tokens->is_placeholder()) {
|
||||
LOG_ERR("%s: image tokens batch is placeholder\n", __func__);
|
||||
return 1;
|
||||
}
|
||||
ok = clip_image_batch_encode(
|
||||
ctx_clip,
|
||||
ctx->n_threads,
|
||||
@@ -1207,24 +1308,17 @@ int mtmd_get_audio_sample_rate(const mtmd_context * ctx) {
|
||||
mtmd_bitmap * mtmd_bitmap_init(uint32_t nx,
|
||||
uint32_t ny,
|
||||
const unsigned char * data) {
|
||||
mtmd_bitmap * bitmap = new mtmd_bitmap;
|
||||
bitmap->nx = nx;
|
||||
bitmap->ny = ny;
|
||||
size_t data_size = (size_t)nx * ny * 3;
|
||||
bitmap->data.resize(data_size);
|
||||
std::memcpy(bitmap->data.data(), data, data_size);
|
||||
mtmd_bitmap * bitmap = new mtmd_bitmap(data, nx, ny);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
mtmd_bitmap * mtmd_bitmap_init_from_audio(size_t n_samples,
|
||||
const float * data) {
|
||||
mtmd_bitmap * bitmap = new mtmd_bitmap;
|
||||
bitmap->nx = n_samples;
|
||||
bitmap->ny = 1;
|
||||
bitmap->is_audio = true;
|
||||
size_t data_size = n_samples * sizeof(float);
|
||||
bitmap->data.resize(data_size);
|
||||
std::memcpy(bitmap->data.data(), data, data_size);
|
||||
mtmd_bitmap * bitmap = new mtmd_bitmap((const unsigned char *)data, n_samples);
|
||||
GGML_ASSERT(bitmap->is_audio);
|
||||
if (!bitmap->is_placeholder()) {
|
||||
GGML_ASSERT(bitmap->get_ro_buf().size() == n_samples * sizeof(float));
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
@@ -1237,11 +1331,11 @@ uint32_t mtmd_bitmap_get_ny(const mtmd_bitmap * bitmap) {
|
||||
}
|
||||
|
||||
const unsigned char * mtmd_bitmap_get_data(const mtmd_bitmap * bitmap) {
|
||||
return bitmap->data.data();
|
||||
return bitmap->get_ro_buf().data();
|
||||
}
|
||||
|
||||
size_t mtmd_bitmap_get_n_bytes(const mtmd_bitmap * bitmap) {
|
||||
return bitmap->data.size();
|
||||
return bitmap->get_ro_buf().size();
|
||||
}
|
||||
|
||||
bool mtmd_bitmap_is_audio(const mtmd_bitmap * bitmap) {
|
||||
@@ -1535,14 +1629,16 @@ void mtmd_debug_encode_image(mtmd_context * ctx, const std::vector<std::vector<f
|
||||
LOG_ERR("%s: model does not support vision input\n", __func__);
|
||||
return;
|
||||
}
|
||||
clip_image_f32 inp_image;
|
||||
inp_image.nx = image.size();
|
||||
inp_image.ny = inp_image.nx;
|
||||
inp_image.buf.reserve(inp_image.nx * inp_image.ny);
|
||||
const int img_sz = (int)image.size();
|
||||
std::vector<float> img_buf;
|
||||
img_buf.reserve(img_sz * img_sz);
|
||||
for (const auto & row : image) {
|
||||
inp_image.buf.insert(inp_image.buf.end(), row.begin(), row.end());
|
||||
img_buf.insert(img_buf.end(), row.begin(), row.end());
|
||||
}
|
||||
LOG_INF("%s: created input image with nx=%d, ny=%d\n", __func__, inp_image.nx, inp_image.ny);
|
||||
clip_image_f32 inp_image;
|
||||
inp_image.set_size({img_sz, img_sz}, false, false);
|
||||
inp_image.cpy_buf(img_buf);
|
||||
LOG_INF("%s: created input image with nx=%d, ny=%d\n", __func__, img_sz, img_sz);
|
||||
mtmd_debug_encode_impl(ctx, ctx->ctx_v, inp_image);
|
||||
}
|
||||
|
||||
@@ -1552,16 +1648,17 @@ void mtmd_debug_encode_audio(mtmd_context * ctx, const std::vector<float> & inpu
|
||||
return;
|
||||
}
|
||||
int n_mel = clip_get_hparams(ctx->ctx_a)->n_mel_bins;
|
||||
clip_image_f32 inp_audio;
|
||||
inp_audio.nx = input.size();
|
||||
inp_audio.ny = n_mel;
|
||||
inp_audio.buf.resize(input.size() * n_mel);
|
||||
for (size_t i = 0; i < input.size(); i++) {
|
||||
const int audio_nx = (int)input.size();
|
||||
std::vector<float> audio_buf(audio_nx * n_mel);
|
||||
for (int i = 0; i < audio_nx; i++) {
|
||||
for (int j = 0; j < n_mel; j++) {
|
||||
inp_audio.buf[j * inp_audio.nx + i] = input[i];
|
||||
audio_buf[j * audio_nx + i] = input[i];
|
||||
}
|
||||
}
|
||||
LOG_INF("%s: created input audio with nx=%d, ny=%d\n", __func__, inp_audio.nx, inp_audio.ny);
|
||||
clip_image_f32 inp_audio;
|
||||
inp_audio.set_size({audio_nx, n_mel}, false, true);
|
||||
inp_audio.cpy_buf(audio_buf);
|
||||
LOG_INF("%s: created input audio with nx=%d, ny=%d\n", __func__, audio_nx, n_mel);
|
||||
mtmd_debug_encode_impl(ctx, ctx->ctx_a, inp_audio);
|
||||
}
|
||||
|
||||
@@ -1571,9 +1668,8 @@ void mtmd_debug_preprocess_image(mtmd_context * ctx, const std::vector<uint8_t>
|
||||
return;
|
||||
}
|
||||
clip_image_u8 img_u8;
|
||||
img_u8.nx = nx;
|
||||
img_u8.ny = ny;
|
||||
img_u8.buf = rgb_values;
|
||||
img_u8.set_size({nx, ny}, false);
|
||||
img_u8.cpy_buf(rgb_values);
|
||||
clip_image_f32_batch batch_f32;
|
||||
GGML_ASSERT(ctx->image_preproc != nullptr);
|
||||
bool ok = ctx->image_preproc->preprocess(img_u8, batch_f32);
|
||||
@@ -1583,7 +1679,7 @@ void mtmd_debug_preprocess_image(mtmd_context * ctx, const std::vector<uint8_t>
|
||||
}
|
||||
LOG_INF("%s: preprocessed image to batch_f32 with %d entries\n", __func__, (int)batch_f32.entries.size());
|
||||
for (size_t i = 0; i < batch_f32.entries.size(); i++) {
|
||||
LOG_INF("%s: entry %zu has nx=%d, ny=%d\n", __func__, i, batch_f32.entries[i]->nx, batch_f32.entries[i]->ny);
|
||||
LOG_INF("%s: entry %zu has nx=%d, ny=%d\n", __func__, i, batch_f32.entries[i]->nx(), batch_f32.entries[i]->ny());
|
||||
// TODO: better way to dump entry content?
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user