ggml-org/llama.cpp · #29622
llama: support both embd + raw tokens in batch
src/llama-arch.cpp15 + / 0 −
@@ -1178,6 +1178,21 @@ bool llm_arch_supports_rs_rollback(const llm_arch & arch) { } } +// these models pick weights, routing or input meaning per ubatch based on token vs embd input+bool llm_arch_supports_mixed_batch(const llm_arch & arch) {+ switch (arch) {+ case LLM_ARCH_COGVLM:+ case LLM_ARCH_DEEPSEEK4:+ case LLM_ARCH_GRANITE_SWITCH:+ case LLM_ARCH_EAGLE3:+ case LLM_ARCH_DFLASH:+ case LLM_ARCH_GEMMA4_ASSISTANT:+ return false;+ default:+ return true;+ }+}+ bool llm_arch_supports_sm_tensor(const llm_arch & arch) { switch (arch) { case LLM_ARCH_GROK:src/llama-arch.h1 + / 0 −
@@ -825,3 +825,4 @@ bool llm_arch_is_hybrid (const llm_arch & arch); bool llm_arch_is_diffusion (const llm_arch & arch); bool llm_arch_supports_sm_tensor(const llm_arch & arch); bool llm_arch_supports_rs_rollback(const llm_arch & arch);+bool llm_arch_supports_mixed_batch(const llm_arch & arch);src/llama-batch.cpp106 + / 39 −
@@ -12,7 +12,7 @@ #include <algorithm> #include <sstream> -llama_batch_allocr::llama_batch_allocr(uint32_t n_pos_per_embd) : n_pos_per_embd(n_pos_per_embd) {+llama_batch_allocr::llama_batch_allocr(uint32_t n_pos_per_embd, bool allow_mixed) : n_pos_per_embd(n_pos_per_embd), allow_mixed(allow_mixed) { const char * LLAMA_BATCH_DEBUG = getenv("LLAMA_BATCH_DEBUG"); debug = LLAMA_BATCH_DEBUG ? atoi(LLAMA_BATCH_DEBUG) : 0; @@ -49,32 +49,60 @@ bool llama_batch_allocr::init( // // determine the content types of the batch // an entry can carry a token id, a token embedding, or both (e.g. MTP hook batches)- // all entries must carry the same combination+ // all entries must carry the same combination, or be a mix of token and embd entries // - const bool has_token = batch_inp.tokens[0].id != LLAMA_TOKEN_NULL;- const bool has_embd = batch_inp.tokens[0].has_embd;+ int32_t n_tok_only = 0;+ int32_t n_embd_only = 0;+ int32_t n_both = 0; - for (int32_t i = 1; i < n_tok; ++i) {- if ((batch_inp.tokens[i].id != LLAMA_TOKEN_NULL) != has_token ||- batch_inp.tokens[i].has_embd != has_embd) {- LLAMA_LOG_ERROR("%s: all entries in the batch must have the same content types\n", __func__);+ for (int32_t i = 0; i < n_tok; ++i) {+ const bool is_tok = batch_inp.tokens[i].id != LLAMA_TOKEN_NULL;+ const bool is_emb = batch_inp.tokens[i].has_embd;++ if (!is_tok && !is_emb) {+ LLAMA_LOG_ERROR("%s: entry %d has neither a token id nor an embedding\n", __func__, i); return false; }++ n_tok_only += is_tok && !is_emb;+ n_embd_only += is_emb && !is_tok;+ n_both += is_tok && is_emb; } - if (!has_token && !has_embd) {- LLAMA_LOG_ERROR("%s: batch has neither token ids nor embeddings\n", __func__);+ if (n_both > 0 && n_both != n_tok) {+ LLAMA_LOG_ERROR("%s: entries with both a token id and an embedding cannot be mixed with other entries\n", __func__); return false; } + const bool mixed = n_tok_only > 0 && n_embd_only > 0;++ if (mixed && !allow_mixed) {+ LLAMA_LOG_ERROR("%s: this model or context does not support batches mixing token and embedding entries\n", __func__);+ return false;+ }++ const bool has_token = n_tok_only > 0 || n_both > 0;+ const bool has_embd = n_embd_only > 0 || n_both > 0;++ if (mixed) {+ is_embd_vec.resize(n_tok);+ for (int32_t i = 0; i < n_tok; ++i) {+ is_embd_vec[i] = batch_inp.tokens[i].has_embd ? 1 : 0;+ }+ }+ // // build flat token/embd array // if (has_token) { token_vec.resize(n_tok); for (int32_t i = 0; i < n_tok; ++i) {+ if (mixed && is_embd_vec[i]) {+ token_vec[i] = 0; // placeholder+ continue;+ } const llama_token id = batch_inp.tokens[i].id; if (id < 0 || id >= batch_inp.n_vocab) { LLAMA_LOG_ERROR("%s: invalid token[%d] = %d\n", __func__, i, id);@@ -84,29 +112,35 @@ bool llama_batch_allocr::init( } } - if (has_embd) {+ if (mixed) {+ embd_vec.assign((size_t) n_tok*n_embd, 0.0f);+ for (int32_t i = 0; i < n_tok; ++i) {+ if (is_embd_vec[i]) {+ const float * src = batch_inp.embd.data() + batch_inp.tokens[i].embd_off;+ std::copy(src, src + n_embd, embd_vec.data() + (size_t) i*n_embd);+ }+ }+ } else if (has_embd) { embd_vec = batch_inp.embd; } //- // build flat pos array- // token batch: pos[i] = tokens[i].pos[0]- // embedding batch: pos[j*n_tok + i] = tokens[i].pos[j] (section-major)+ // build flat pos array, section-major: pos[j*n_tok + i] = section j of entry i+ // token entry: [p, p, p, 0] (M-RoPE text position)+ // embd entry: tokens[i].pos as-is // - {- const int32_t n_pos_total = has_token ? n_tok : n_tok * (int32_t) n_pos_per_embd;- pos.resize(n_pos_total);- if (has_token) {- for (int32_t i = 0; i < n_tok; ++i) {- pos[i] = batch_inp.tokens[i].pos[0];- }- } else {- for (int32_t i = 0; i < n_tok; ++i) {- for (uint32_t j = 0; j < n_pos_per_embd; ++j) {- pos[(int32_t) j * n_tok + i] = batch_inp.tokens[i].pos[j];- }+ pos.resize((size_t) n_tok*n_pos_per_embd);+ for (int32_t i = 0; i < n_tok; ++i) {+ const auto & tok = batch_inp.tokens[i];+ const bool expand = tok.id != LLAMA_TOKEN_NULL;+ for (uint32_t j = 0; j < n_pos_per_embd; ++j) {+ llama_pos p = tok.pos[j];+ if (expand) {+ // expand [p] to [p, p, p, 0] for M-RoPE+ p = j < 3 ? tok.pos[0] : 0; }+ pos[(size_t) j*n_tok + i] = p; } } @@ -264,6 +298,7 @@ bool llama_batch_allocr::init( /*.seq_id_unq =*/ this->seq_id_unq.data(), /*.seq_idx =*/ this->seq_idx.data(), /*.output =*/ batch.logits,+ /*.type =*/ is_embd_vec.empty() ? nullptr : is_embd_vec.data(), /*.decision_order =*/ decision_order.empty() ? nullptr : decision_order.data(), /*.data =*/ {}, };@@ -294,6 +329,21 @@ bool llama_batch_allocr::init( // if (n_pos_per_embd > 1) {+ // in a mixed batch, the first entry of each seq picks the rule+ std::vector<int8_t> seq_first_embd(n_seq_max, batch.token ? 0 : 1);+ if (mixed) {+ std::vector<bool> seen(n_seq_max, false);+ for (int32_t i = 0; i < batch.n_tokens; ++i) {+ for (int32_t s = 0; s < batch.n_seq_id[i]; ++s) {+ const llama_seq_id sid = batch.seq_id[i][s];+ if (!seen[sid]) {+ seen[sid] = true;+ seq_first_embd[sid] = is_embd_vec[i];+ }+ }+ }+ }+ // M-RoPE case: allow position to "jump" forward only (non-continuous positions are allowed) for (uint32_t s = 0; s < n_seq_max; ++s) { if (seq_pos[s].empty()) {@@ -302,7 +352,7 @@ bool llama_batch_allocr::init( const llama_pos p0 = mem ? mem->seq_pos_max(s) : -1; - if (batch.token) {+ if (!seq_first_embd[s]) { if (p0 >= 0 && p0 >= seq_pos_min(s)) { LLAMA_LOG_ERROR( "%s: the tokens of sequence %d in the input batch have inconsistent sequence positions:\n"@@ -471,6 +521,7 @@ llama_ubatch llama_batch_allocr::ubatch_reserve(uint32_t n_seq_tokens, uint32_t /*.seq_id_unq =*/ udata->seq_id_unq.data(), /*.seq_idx =*/ udata->seq_idx.data(), /*.output =*/ udata->output.data(),+ /*.type =*/ nullptr, /*.decision_order =*/ nullptr, /*.data =*/ std::move(udata), };@@ -769,6 +820,7 @@ void llama_batch_allocr::clear() { token_vec .clear(); embd_vec .clear();+ is_embd_vec .clear(); seq_id_data .clear(); pos .clear(); n_seq_id .clear();@@ -799,7 +851,20 @@ llama_ubatch llama_batch_allocr::ubatch_add(const std::vector<int32_t> & idxs, u auto udata = std::make_shared<llama_ubatch::data_t>(); - const int64_t n_embd_all = batch.embd ? (int64_t) n_tokens*n_embd : 0;+ const bool mixed_batch = !is_embd_vec.empty();++ // a ubatch with a single kind of rows is emitted as a plain token or embd ubatch+ uint32_t n_embd_rows = 0;+ if (mixed_batch) {+ for (int32_t idx : idxs) {+ n_embd_rows += is_embd_vec[idx];+ }+ }+ const bool mixed = mixed_batch && n_embd_rows > 0 && n_embd_rows < n_tokens;+ const bool use_token = batch.token && !(mixed_batch && n_embd_rows == n_tokens);+ const bool use_embd = batch.embd && !(mixed_batch && n_embd_rows == 0);++ const int64_t n_embd_all = use_embd ? (int64_t) n_tokens*n_embd : 0; const int64_t n_pos_all = (int64_t) n_tokens*n_pos_per_embd; udata->token .resize(n_tokens);@@ -810,6 +875,7 @@ llama_ubatch llama_batch_allocr::ubatch_add(const std::vector<int32_t> & idxs, u udata->seq_id_unq.resize(0); udata->seq_idx .resize(LLAMA_MAX_SEQ, -1); udata->output .resize(n_tokens);+ udata->type .resize(mixed ? n_tokens : 0); udata->decision_order.resize(decision_order.empty() ? 0 : n_tokens); udata->batch_idxs = idxs;@@ -818,21 +884,20 @@ llama_ubatch llama_batch_allocr::ubatch_add(const std::vector<int32_t> & idxs, u seq_set_t seq_set_unq; for (size_t i = 0; i < idxs.size(); ++i) {- if (batch.token) {+ if (use_token) { udata->token[i] = batch.token[idxs[i]]; } - if (batch.embd) {+ if (use_embd) { memcpy(udata->embd.data() + i*n_embd, batch.embd + (int64_t) idxs[i]*n_embd, n_embd*sizeof(float)); } + if (mixed) {+ udata->type[i] = is_embd_vec[idxs[i]];+ }+ for (size_t j = 0; j < (size_t)n_pos_per_embd; ++j) {- // if we are using M-RoPE- // if the current batch is text, we need to broadcast the same position across all RoPE sections- // otherwise, the input batch is image embeddings, we copy the positions as-is- // if we are not using M-RoPE, there is only one position per token (this loop runs only once)- size_t src_off = batch.token ? 0 : j*batch.n_tokens;- udata->pos[j*n_tokens + i] = batch.pos[src_off + idxs[i]];+ udata->pos[j*n_tokens + i] = batch.pos[j*batch.n_tokens + idxs[i]]; } udata->n_seq_id[i] = batch.n_seq_id[idxs[i]];@@ -875,14 +940,15 @@ llama_ubatch llama_batch_allocr::ubatch_add(const std::vector<int32_t> & idxs, u /*.n_seqs_unq =*/ (uint32_t) udata->seq_id_unq.size(), /*.n_pos =*/ n_pos_per_embd, - /*.token =*/ batch.token ? udata->token.data() : nullptr,- /*.embd =*/ batch.embd ? udata->embd.data() : nullptr,+ /*.token =*/ use_token ? udata->token.data() : nullptr,+ /*.embd =*/ use_embd ? udata->embd.data() : nullptr, /*.pos =*/ udata->pos.data(), /*.n_seq_id =*/ udata->n_seq_id.data(), /*.seq_id =*/ udata->seq_id.data(), /*.seq_id_unq =*/ udata->seq_id_unq.data(), /*.seq_idx =*/ udata->seq_idx.data(), /*.output =*/ udata->output.data(),+ /*.type =*/ mixed ? udata->type.data() : nullptr, /*.decision_order =*/ udata->decision_order.empty() ? nullptr : udata->decision_order.data(), /*.data =*/ std::move(udata), };@@ -933,6 +999,7 @@ void llama_batch_allocr::ubatch_print(const llama_ubatch & ubatch, int debug) { LLAMA_LOG_DEBUG("%s: seq_id_unq = %s\n", __func__, ss_seq_id_unq.str().c_str()); LLAMA_LOG_DEBUG("%s: seq_idx = %s\n", __func__, ss_seq_idx.str().c_str()); LLAMA_LOG_DEBUG("%s: output = %p\n", __func__, (void *) ubatch.output);+ LLAMA_LOG_DEBUG("%s: type = %p\n", __func__, (void *) ubatch.type); LLAMA_LOG_DEBUG("%s: n_outputs = %d\n", __func__, n_outputs); if (debug > 0) {@@ -963,7 +1030,7 @@ void llama_batch_allocr::ubatch_print(const llama_ubatch & ubatch, int debug) { } } - if (ubatch.token) {+ if (ubatch.token && !(ubatch.is_mixed() && ubatch.type[i])) { LLAMA_LOG_DEBUG("%s: %4d: id = %6d (%16s), pos = %4d, n_seq_id = %2d, seq_id = [%s], output = %d\n", __func__, i, ubatch.token[i], vocab->token_to_piece(ubatch.token[i]).c_str(), ubatch.pos[i], ubatch.n_seq_id[i], ss.str().c_str(), ubatch.output[i]);src/llama-batch.h14 + / 1 −
@@ -29,6 +29,11 @@ struct llama_ubatch { return n_pos >= 3; } + // mixed: type picks token or embd per row, pos has n_pos sections for all rows+ bool is_mixed() const {+ return type != nullptr;+ }+ uint32_t b_equal_seqs; // note: this is a boolean, but we use an int32_t for alignment // otherwise address sanitizer complains // TODO: whole_seqs for embeddings?@@ -52,6 +57,7 @@ struct llama_ubatch { llama_seq_id * seq_id_unq; // [n_seqs_unq] | s | seq_id int32_t * seq_idx; // [LLAMA_MAX_SEQ] | - | seq_idx int8_t * output; // [n_tokens] | i | -+ int8_t * type; // [n_tokens] | i | - (mixed ubatch only, 0 - token, 1 - embd) int32_t * decision_order; // [n_tokens], NULL if no entry has one, see llama_batch_ext_set_decision_order() struct data_t {@@ -63,6 +69,7 @@ struct llama_ubatch { std::vector<llama_seq_id> seq_id_unq; std::vector<int32_t> seq_idx; std::vector<int8_t> output;+ std::vector<int8_t> type; std::vector<int32_t> batch_idxs; // original batch index for each token std::vector<int32_t> decision_order; @@ -73,6 +80,9 @@ struct llama_ubatch { std::shared_ptr<data_t> data; }; +// crash if a mixed ubatch reaches code that expects only tokens or only embd+#define ASSERT_EMBD_OR_TOKEN(ubatch) GGML_ASSERT(!(ubatch).is_mixed() && "mixed token/embd ubatch is not supported here")+ struct llama_hparams; // MTP hook batches carry the target model's hidden state (n_embd_out size).@@ -134,7 +144,7 @@ struct llama_batch_ext { // a helper for sanitizing, fulfilling and splitting a batch class llama_batch_allocr { public:- llama_batch_allocr(uint32_t n_pos_per_embd);+ llama_batch_allocr(uint32_t n_pos_per_embd, bool allow_mixed = false); // convert a llama_batch_ext to internal llama_batch and sanitize it bool init(@@ -192,12 +202,15 @@ class llama_batch_allocr { // ref: https://github.com/ggml-org/llama.cpp/issues/13694#issuecomment-2983871762 const uint32_t n_pos_per_embd; + const bool allow_mixed;+ uint32_t n_embd; uint32_t n_seq_max; uint32_t n_outputs; std::vector<llama_token> token_vec; // owned token IDs built from llama_batch_ext std::vector<float> embd_vec; // owned embeddings built from llama_batch_ext+ std::vector<int8_t> is_embd_vec; // mixed batch only (= 1 if embd, 0 if text token) std::vector<llama_seq_id> seq_id_data; // flat storage for seq_id pointers below std::vector<llama_pos> pos;src/llama-context.cpp3 + / 1 −
@@ -87,7 +87,9 @@ llama_context::llama_context( model(model), cvec(std::make_unique<llama_adapter_cvec>()), loras(std::make_unique<llama_adapter_loras>()),- balloc(std::make_unique<llama_batch_allocr>(model.hparams.n_pos_per_embd())) {+ // MTP uses the embd input for the hidden state+ balloc(std::make_unique<llama_batch_allocr>(model.hparams.n_pos_per_embd(),+ llm_arch_supports_mixed_batch(model.arch) && params.ctx_type == LLAMA_CONTEXT_TYPE_DEFAULT)) { // TODO warning when creating llama_context with awkward ctx size that is not a power of 2, // may need to be backend-dependent LLAMA_LOG_INFO("%s: constructing llama_context\n", __func__);src/llama-graph.cpp105 + / 34 −
@@ -73,25 +73,72 @@ void llm_graph_input_embd::set_input(const llama_ubatch * ubatch) { ggml_backend_tensor_set(tokens, ubatch->token, 0, n_tokens*ggml_element_size(tokens)); } - if (ubatch->embd) {+ if (ubatch->embd && embd && !ubatch->is_mixed()) { GGML_ASSERT(n_embd == embd->ne[0]); const int64_t n_tokens = ubatch->n_tokens; ggml_backend_tensor_set(embd, ubatch->embd, 0, n_tokens*n_embd*ggml_element_size(embd)); }++ if (ubatch->is_mixed() && embd) {+ GGML_ASSERT(mixed_tokens && mixed_slots && mixed_embd && "mixed token/embd ubatch is not supported here");++ std::vector<int32_t> ids;+ std::vector<int64_t> slots;+ for (uint32_t i = 0; i < ubatch->n_tokens; ++i) {+ if (!ubatch->type[i]) {+ ids.push_back(ubatch->token[i]);+ slots.push_back(i);+ }+ }+ GGML_ASSERT((int64_t) ids.size() == mixed_tokens->ne[0]);+ GGML_ASSERT(n_embd == mixed_embd->ne[0]);++ ggml_backend_tensor_set(mixed_tokens, ids.data(), 0, ggml_nbytes(mixed_tokens));+ ggml_backend_tensor_set(mixed_slots, slots.data(), 0, ggml_nbytes(mixed_slots));+ ggml_backend_tensor_set(mixed_embd, ubatch->embd, 0, ggml_nbytes(mixed_embd));+ }++ if (scale_rows) {+ const int64_t n_tokens = ubatch->n_tokens;++ std::vector<float> data(n_tokens);+ for (int64_t i = 0; i < n_tokens; ++i) {+ const bool is_embd = !ubatch->token || (ubatch->is_mixed() && ubatch->type[i]);+ data[i] = is_embd ? 1.0f : scale_tok;+ }+ ggml_backend_tensor_set(scale_rows, data.data(), 0, ggml_nbytes(scale_rows));+ }+}++// number of token rows of the mixed path, a non-mixed ubatch is sized for the worst case+static int64_t llm_graph_n_tok_rows(const llama_ubatch & ubatch) {+ if (!ubatch.is_mixed()) {+ return ubatch.n_tokens;+ }+ int64_t n = 0;+ for (uint32_t i = 0; i < ubatch.n_tokens; ++i) {+ n += !ubatch.type[i];+ }+ return n; } bool llm_graph_input_embd::can_reuse(const llm_graph_params & params) { bool res = true; res &= (!params.ubatch.token) || (tokens && tokens->ne[0] == params.ubatch.n_tokens); res &= (!params.ubatch.embd) || (embd && embd->ne[1] == params.ubatch.n_tokens);+ res &= (!mixed_tokens) || mixed_tokens->ne[0] == llm_graph_n_tok_rows(params.ubatch);+ res &= (!mixed_embd) || mixed_embd->ne[1] == params.ubatch.n_tokens;+ res &= (!scale_rows) || scale_rows->ne[1] == params.ubatch.n_tokens; return res; } void llm_graph_input_embd_h::set_input(const llama_ubatch * ubatch) {+ ASSERT_EMBD_OR_TOKEN(*ubatch);+ const int64_t n_tokens = ubatch->n_tokens; if (ubatch->token) {@@ -128,21 +175,7 @@ void llm_graph_input_pos::set_input(const llama_ubatch * ubatch) { if (ubatch->pos && pos) { const int64_t n_tokens = ubatch->n_tokens; - if (ubatch->token && n_pos_per_embd == 4) {- // in case we're using M-RoPE with text tokens, convert the 1D positions to 4D- // the 3 first dims are the same, and 4th dim is all 0- std::vector<llama_pos> pos_data(n_tokens*n_pos_per_embd);- // copy the first dimension- for (int i = 0; i < n_tokens; ++i) {- pos_data[ i] = ubatch->pos[i];- pos_data[ n_tokens + i] = ubatch->pos[i];- pos_data[2 * n_tokens + i] = ubatch->pos[i];- pos_data[3 * n_tokens + i] = 0; // 4th dim is 0- }- ggml_backend_tensor_set(pos, pos_data.data(), 0, pos_data.size()*ggml_element_size(pos));- } else {- ggml_backend_tensor_set(pos, ubatch->pos, 0, n_tokens*n_pos_per_embd*ggml_element_size(pos));- }+ ggml_backend_tensor_set(pos, ubatch->pos, 0, n_tokens*n_pos_per_embd*ggml_element_size(pos)); } } @@ -2377,7 +2410,7 @@ ggml_tensor * llm_graph_context::build_moe_ffn( } // input embeddings with optional lora-ggml_tensor * llm_graph_context::build_inp_embd(ggml_tensor * tok_embd) const {+ggml_tensor * llm_graph_context::build_inp_embd(ggml_tensor * tok_embd, float tok_scale) const { const int64_t n_embd_inp = hparams.n_embd_inp(); const int64_t n_embd = hparams.n_embd; @@ -2394,15 +2427,9 @@ ggml_tensor * llm_graph_context::build_inp_embd(ggml_tensor * tok_embd) const { cb(inp->embd, "inp_embd", -1); ggml_set_input(inp->embd); - // select one of the 2 inputs, based on the batch contents- // ref: https://github.com/ggml-org/llama.cpp/pull/18550- std::array<ggml_tensor *, 2> inps;-- // token embeddings path (ubatch.token != nullptr)- {- auto & cur = inps[0];-- cur = ggml_get_rows(ctx0, tok_embd, inp->tokens);+ // token embeddings with lora and padding+ auto build_tok = [&](ggml_tensor * ids) {+ ggml_tensor * cur = ggml_get_rows(ctx0, tok_embd, ids); // apply lora for embedding tokens if needed for (const auto & lora : *loras) {@@ -2416,7 +2443,7 @@ ggml_tensor * llm_graph_context::build_inp_embd(ggml_tensor * tok_embd) const { ggml_tensor * inpL_delta = ggml_scale(ctx0, ggml_mul_mat( ctx0, lw->b, // non-transposed lora_b- ggml_get_rows(ctx0, lw->a, inp->tokens)+ ggml_get_rows(ctx0, lw->a, ids) ), scale); cur = ggml_add(ctx0, cur, inpL_delta);@@ -2425,36 +2452,80 @@ ggml_tensor * llm_graph_context::build_inp_embd(ggml_tensor * tok_embd) const { if (n_embd_inp != n_embd) { cur = ggml_pad(ctx0, cur, hparams.n_embd_inp() - n_embd, 0, 0, 0); }- }++ return cur;+ };++ // select one of the 3 inputs, based on the batch contents+ // ref: https://github.com/ggml-org/llama.cpp/pull/18550+ std::array<ggml_tensor *, 3> inps = {};++ // token embeddings path (ubatch.token != nullptr)+ inps[0] = build_tok(inp->tokens); // vector embeddings path (ubatch.embd != nullptr)- {- auto & cur = inps[1];+ inps[1] = inp->embd;++ // mixed path (ubatch.is_mixed()): set_rows the token rows into a copy of the embd rows, with its own inputs as select branches must not share tensors+ // TODO: use inp->tokens and inp->embd once ggml_build_forward_select allows it+ const bool has_mixed = llm_arch_supports_mixed_batch(arch) && cparams.ctx_type == LLAMA_CONTEXT_TYPE_DEFAULT;+ if (has_mixed) {+ const int64_t n_tok_rows = llm_graph_n_tok_rows(ubatch);++ inp->mixed_tokens = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_tok_rows);+ cb(inp->mixed_tokens, "inp_mixed_tokens", -1);+ ggml_set_input(inp->mixed_tokens); - cur = inp->embd;+ inp->mixed_slots = ggml_new_tensor_1d(ctx0, GGML_TYPE_I64, n_tok_rows);+ cb(inp->mixed_slots, "inp_mixed_slots", -1);+ ggml_set_input(inp->mixed_slots);++ inp->mixed_embd = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, n_embd_inp, ubatch.n_tokens);+ cb(inp->mixed_embd, "inp_mixed_embd", -1);+ ggml_set_input(inp->mixed_embd);++ // note: set_rows writes into its destination, so it gets a copy of the input+ inps[2] = ggml_set_rows(ctx0, ggml_dup(ctx0, inp->mixed_embd), build_tok(inp->mixed_tokens), inp->mixed_slots); } assert(ggml_are_same_shape (inps[0], inps[1])); assert(ggml_are_same_stride(inps[0], inps[1])); - ggml_tensor * cur = ggml_build_forward_select(gf, inps.data(), inps.size(), ubatch.token ? 0 : 1);+ const int idx = ubatch.is_mixed() ? 2 : ubatch.token ? 0 : 1;++ ggml_tensor * cur = ggml_build_forward_select(gf, inps.data(), has_mixed ? 3 : 2, idx); if (n_embd_inp != n_embd) { cur = ggml_view_2d(ctx0, cur, n_embd, n_tokens, cur->nb[1], 0); } res->t_inp_embd = cur; - // For Granite architecture // NOTE: For deepstack models, only apply scale to token inputs (ie text-only input). // Raw embeddings are assumed to be multimodal inputs that should not be scaled.- if (hparams.f_embedding_scale != 0.0f && (ubatch.token || hparams.n_deepstack_layers == 0)) {+ const bool scale_tok_only = hparams.f_embedding_scale != 0.0f && hparams.n_deepstack_layers > 0;++ // For Granite architecture+ if (hparams.f_embedding_scale != 0.0f && !scale_tok_only) { if (!ggml_is_contiguous(cur)) { cur = ggml_cont(ctx0, cur); } cur = ggml_scale(ctx0, cur, hparams.f_embedding_scale); } + // scale the token rows only, applied after the select so that the graph is the same for any batch contents+ inp->scale_tok = tok_scale*(scale_tok_only ? hparams.f_embedding_scale : 1.0f);+ if (inp->scale_tok != 1.0f) {+ inp->scale_rows = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, 1, ubatch.n_tokens);+ cb(inp->scale_rows, "inp_scale_rows", -1);+ ggml_set_input(inp->scale_rows);++ if (!ggml_is_contiguous(cur)) {+ cur = ggml_cont(ctx0, cur);+ }+ cur = ggml_mul(ctx0, cur, inp->scale_rows);+ }+ cb(cur, "embd", -1); res->add_input(std::move(inp));src/llama-graph.h11 + / 3 −
@@ -134,8 +134,14 @@ class llm_graph_input_embd : public llm_graph_input_i { bool can_reuse(const llm_graph_params & params) override; - ggml_tensor * tokens = nullptr; // I32 [n_batch]- ggml_tensor * embd = nullptr; // F32 [n_embd, n_batch]+ ggml_tensor * tokens = nullptr; // I32 [n_batch]+ ggml_tensor * embd = nullptr; // F32 [n_embd, n_batch]+ ggml_tensor * mixed_tokens = nullptr; // I32 [n_tok_rows], mixed path: ids of the token rows+ ggml_tensor * mixed_slots = nullptr; // I64 [n_tok_rows], mixed path: batch index of the token rows+ ggml_tensor * mixed_embd = nullptr; // F32 [n_embd, n_batch], mixed path: embd rows, token rows are overwritten+ ggml_tensor * scale_rows = nullptr; // F32 [1, n_batch], per-row scale: scale_tok for token rows, 1 for embd rows++ float scale_tok = 1.0f; const int64_t n_embd = 0; };@@ -823,6 +829,7 @@ struct llm_graph_params { ubatch.n_seq_tokens == other.ubatch.n_seq_tokens && ubatch.n_seqs == other.ubatch.n_seqs && ubatch.n_seqs_unq == other.ubatch.n_seqs_unq &&+ ubatch.is_mixed() == other.ubatch.is_mixed() && ( (!ubatch.token && !other.ubatch.token) || (!ubatch.embd && !other.ubatch.embd) ||@@ -1166,7 +1173,8 @@ struct llm_graph_context { // inputs // - ggml_tensor * build_inp_embd(ggml_tensor * tok_embd) const;+ // tok_scale: applied to token rows only+ ggml_tensor * build_inp_embd(ggml_tensor * tok_embd, float tok_scale = 1.0f) const; ggml_tensor * build_inp_pos() const; ggml_tensor * build_inp_attn_scale() const; ggml_tensor * build_inp_out_ids() const;src/llama-kv-cache-dsv4.cpp2 + / 0 −
@@ -83,6 +83,7 @@ static llama_ubatch dsv4_build_raw_write_ubatch(const llama_ubatch & ubatch) { if (!dsv4_ubatch_has_coupled(ubatch)) { return ubatch; }+ ASSERT_EMBD_OR_TOKEN(ubatch); if (ubatch.embd) { throw std::runtime_error("DSV4 coupled embedding ubatches are not supported"); }@@ -164,6 +165,7 @@ static llama_ubatch dsv4_build_raw_write_ubatch(const llama_ubatch & ubatch) { /*.seq_id_unq =*/ data->seq_id_unq.data(), /*.seq_idx =*/ data->seq_idx.data(), /*.output =*/ data->output.data(),+ /*.type =*/ nullptr, /*.decision_order =*/ nullptr, /*.data =*/ data, };src/llama-kv-cache.cpp8 + / 3 −
@@ -1138,7 +1138,9 @@ void llama_kv_cache::apply_ubatch(const slot_info & sinfo, const llama_ubatch & ext.y = ubatch.pos[i + ubatch.n_tokens]; } - if (ubatch.token) {+ const bool is_embd = !ubatch.token || (ubatch.is_mixed() && ubatch.type[i]);++ if (!is_embd) { ext.tok = ubatch.token[i]; } else if (hparams.ple_n_heads > 0) { // embd batch (multimodal input) has no token ids, need to pad it with the correct ID for PLE layers@@ -1862,10 +1864,13 @@ void llama_kv_cache::get_prev_tokens(const llama_ubatch & ubatch, uint32_t n, st // an embd (multimodal) ubatch can repeat one position for a whole image, so positions // do not encode the token order; resolve its predecessors by ubatch order instead+ // same for a mixed ubatch+ const bool by_order = !ubatch.token || ubatch.is_mixed();+ std::vector<uint32_t> ord; // index among the ubatch tokens of the same seq std::unordered_map<llama_seq_id, std::vector<uint32_t>> seq_idx; - if (!ubatch.token) {+ if (by_order) { ord.resize(n_tokens); for (uint32_t i = 0; i < n_tokens; ++i) { auto & v = seq_idx[ubatch.seq_id[i][0]];@@ -1883,7 +1888,7 @@ void llama_kv_cache::get_prev_tokens(const llama_ubatch & ubatch, uint32_t n, st const llama_pos d = (llama_pos) (n - j); llama_pos p;- if (!ubatch.token) {+ if (by_order) { const auto & v = seq_idx[seq_id]; const int64_t k = (int64_t) ord[i] - d; // k >= 0: an earlier token of this very ubatch; k < 0: before the chunksrc/models/cogvlm.cpp1 + / 0 −
@@ -70,6 +70,7 @@ llama_model_cogvlm::graph::graph(const llama_model & model, const llm_graph_para // check ubatch to see if we have input tokens (text) // or an input embedding vector (image) bool is_text;+ ASSERT_EMBD_OR_TOKEN(ubatch); if (ubatch.token) { is_text = true; } else {src/models/cohere2moe.cpp1 + / 0 −
@@ -317,6 +317,7 @@ llama_model_cohere2moe::graph_mtp::graph_mtp(const llama_model & model, const ll // TODO: make static using `ggml_build_forward_select()` // see llm_graph_context::build_inp_embd() for reference ggml_tensor * tok_embd;+ ASSERT_EMBD_OR_TOKEN(ubatch); if (ubatch.token) { ggml_tensor * tok_embd_w = layer.nextn.embed_tokens ? layer.nextn.embed_tokens : model.tok_embd; tok_embd = ggml_get_rows(ctx0, tok_embd_w, inp->tokens);src/models/deepseek2.cpp1 + / 0 −
@@ -221,6 +221,7 @@ llama_model_deepseek2::graph_mtp::graph_mtp(const llama_model & model, const llm ggml_set_input(inp->embd); ggml_tensor * tok_embd;+ ASSERT_EMBD_OR_TOKEN(ubatch); if (ubatch.token) { ggml_tensor * tok_embd_w = layer.nextn.embed_tokens ? layer.nextn.embed_tokenssrc/models/deepseek32.cpp1 + / 0 −
@@ -535,6 +535,7 @@ llama_model_deepseek32::graph_mtp::graph_mtp(const llama_model & model, const ll ggml_set_input(inp->embd); ggml_tensor * tok_embd;+ ASSERT_EMBD_OR_TOKEN(ubatch); if (ubatch.token) { ggml_tensor * tok_embd_w = layer.nextn.embed_tokens ? layer.nextn.embed_tokens : model.tok_embd; src/models/deepseek4.cpp2 + / 0 −
@@ -1281,6 +1281,7 @@ llama_model_deepseek4::graph::graph(const llama_model & model, const llm_graph_p ggml_tensor * exp_probs_b = layer.ffn_exp_probs_b; // may apply exp_probs_b_vl is input is from mtmd+ ASSERT_EMBD_OR_TOKEN(ubatch); const bool is_media = ubatch.embd != nullptr; if (is_media) { if (layer.ffn_exp_probs_b_vl) {@@ -1366,6 +1367,7 @@ llama_model_deepseek4::graph_mtp::graph_mtp(const llama_model & model, const llm GGML_ASSERT(cparams.nextn_layer_offset >= 0 && cparams.nextn_layer_offset < (int) hparams.n_layer_nextn && "nextn_layer_offset out of range [0, n_layer_nextn)");+ ASSERT_EMBD_OR_TOKEN(ubatch); GGML_ASSERT(ubatch.token && "DEEPSEEK4 MTP requires token input"); const int64_t hc = hparams.dsv4_hc_mult;src/models/dflash.cpp2 + / 0 −
@@ -604,6 +604,7 @@ llama_model_dflash::graph<false>::graph(const llama_model & model, const llm_gra }; // KV cache injection+ ASSERT_EMBD_OR_TOKEN(ubatch); if (ubatch.embd) { auto inp = std::make_unique<llm_graph_input_embd>(n_embd_inp); @@ -870,6 +871,7 @@ llama_model_dflash::graph_dsv4::graph_dsv4(const llama_model & model, const llm_ llm_graph_input_attn_k_iswa * inp_attn = build_attn_inp_k_iswa(); // KV cache injection: fused target features from the encoder+ ASSERT_EMBD_OR_TOKEN(ubatch); if (ubatch.embd) { auto inp = std::make_unique<llm_graph_input_embd>(n_embd_inp); src/models/gemma-embedding.cpp1 + / 3 −
@@ -77,10 +77,8 @@ llama_model_gemma_embedding::graph::graph(const llama_model & model, const llm_g ggml_tensor * cur; ggml_tensor * inpL; - inpL = build_inp_embd(model.tok_embd);- // important: do not normalize weights for raw embeddings input (i.e. encoded image embeddings)- inpL = ggml_scale(ctx0, inpL, ubatch.token ? sqrtf(n_embd) : 1.0f);+ inpL = build_inp_embd(model.tok_embd, sqrtf(n_embd)); cb(inpL, "inp_scaled", -1); // inp_pos - contains the positionssrc/models/gemma3.cpp1 + / 3 −
@@ -85,10 +85,8 @@ llama_model_gemma3::graph<iswa>::graph(const llama_model & model, const llm_grap ggml_tensor * cur; ggml_tensor * inpL; - inpL = build_inp_embd(model.tok_embd);- // important: do not normalize weights for raw embeddings input (i.e. encoded image embeddings)- inpL = ggml_scale(ctx0, inpL, ubatch.token ? sqrtf(n_embd) : 1.0f);+ inpL = build_inp_embd(model.tok_embd, sqrtf(n_embd)); cb(inpL, "inp_scaled", -1); // inp_pos - contains the positionssrc/models/gemma3n.cpp2 + / 3 −
@@ -96,10 +96,8 @@ llama_model_gemma3n::graph::graph(const llama_model & model, const llm_graph_par ggml_tensor * cur; ggml_tensor * inpL; - inpL = build_inp_embd(model.tok_embd);- // important: do not normalize weights for raw embeddings input (i.e. encoded image embeddings)- inpL = ggml_scale(ctx0, inpL, ubatch.token ? sqrtf(n_embd) : 1.0f);+ inpL = build_inp_embd(model.tok_embd, sqrtf(n_embd)); cb(inpL, "inp_scaled", -1); // inp_pos - contains the positions@@ -325,6 +323,7 @@ ggml_tensor * llama_model_gemma3n::graph::build_inp_per_layer() { auto inp = std::make_unique<llm_graph_input_embd>(n_embd); ggml_tensor * inp_per_layer; float tok_embd_scale = sqrtf((float) n_embd_altup);+ // mixed ubatch: embd rows have token id 0, same padding row as below if (ubatch.token) { inp->tokens = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, ubatch.n_tokens); ggml_set_input(inp->tokens);src/models/gemma4.cpp2 + / 3 −
@@ -159,10 +159,8 @@ llama_model_gemma4::graph::graph(const llama_model & model, const llm_graph_para ggml_tensor * cur; ggml_tensor * inpL; - inpL = build_inp_embd(model.tok_embd);- // important: do not normalize weights for raw embeddings input (i.e. encoded image emdeddings)- inpL = ggml_scale(ctx0, inpL, ubatch.token ? sqrtf(n_embd) : 1.0f);+ inpL = build_inp_embd(model.tok_embd, sqrtf(n_embd)); cb(inpL, "inp_scaled", -1); // inp_pos - contains the positions@@ -473,6 +471,7 @@ ggml_tensor * llama_model_gemma4::graph::build_inp_per_layer() { ggml_tensor * inp_per_layer; float tok_embd_scale = sqrtf((float) n_embd_per_layer);+ // mixed ubatch: embd rows have token id 0, same padding row as below if (ubatch.token) { inp->tokens = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, ubatch.n_tokens); ggml_set_input(inp->tokens);src/models/glm-dsa.cpp1 + / 0 −
@@ -579,6 +579,7 @@ llama_model_glm_dsa::graph_mtp::graph_mtp(const llama_model & model, const llm_g ggml_set_input(inp->embd); ggml_tensor * tok_embd;+ ASSERT_EMBD_OR_TOKEN(ubatch); if (ubatch.token) { ggml_tensor * tok_embd_w = layer.nextn.embed_tokens ? layer.nextn.embed_tokens : model.tok_embd; src/models/glm4-moe.cpp1 + / 0 −
@@ -158,6 +158,7 @@ llama_model_glm4_moe::graph_mtp::graph_mtp(const llama_model & model, const llm_ ggml_set_input(inp->embd); ggml_tensor * tok_embd;+ ASSERT_EMBD_OR_TOKEN(ubatch); if (ubatch.token) { ggml_tensor * tok_embd_w = layer.nextn.embed_tokens ? layer.nextn.embed_tokens : model.tok_embd; tok_embd = ggml_get_rows(ctx0, tok_embd_w, inp->tokens);src/models/granite-switch.cpp2 + / 0 −
@@ -149,6 +149,7 @@ class llm_graph_input_switch : public llm_graph_input_i { // K dim-0 is +gain for an adapter token, -gain otherwise; the causal softmax then // lets a single visible adapter token dominate so the readback recovers its slot. void llm_graph_input_switch::set_input(const llama_ubatch * ubatch) {+ ASSERT_EMBD_OR_TOKEN(*ubatch); if (!ubatch->token) { return; }@@ -226,6 +227,7 @@ llama_model_granite_switch::graph::graph( const auto & smodel = static_cast<const llama_model_granite_switch &>(model); // TODO: support raw embedding input (multimodal / pre-embedded tokens) when needed+ ASSERT_EMBD_OR_TOKEN(ubatch); GGML_ASSERT(ubatch.token && "granite-switch requires token input"); const int64_t n_embd_head = hparams.n_embd_head_v();src/models/nemotron-h-moe.cpp1 + / 0 −
@@ -34,6 +34,7 @@ llama_model_nemotron_h_moe::graph_mtp::graph_mtp(const llama_model & model, cons ggml_set_input(inp->embd); ggml_tensor * tok_embd;+ ASSERT_EMBD_OR_TOKEN(ubatch); if (ubatch.token) { tok_embd = ggml_get_rows(ctx0, tok_embd_w, inp->tokens); } else {src/models/qwen35.cpp1 + / 0 −
@@ -529,6 +529,7 @@ llama_model_qwen35::graph_mtp::graph_mtp(const llama_model & model, const llm_gr // TODO: make static using `ggml_build_forward_select()` // see llm_graph_context::build_inp_embd() for reference ggml_tensor * tok_embd;+ ASSERT_EMBD_OR_TOKEN(ubatch); if (ubatch.token) { ggml_tensor * tok_embd_w = layer.nextn.embed_tokens ? layer.nextn.embed_tokens : model.tok_embd; src/models/qwen35moe.cpp1 + / 0 −
@@ -579,6 +579,7 @@ llama_model_qwen35moe::graph_mtp::graph_mtp(const llama_model & model, const llm // TODO: make static using `ggml_build_forward_select()` // see llm_graph_context::build_inp_embd() for reference ggml_tensor * tok_embd;+ ASSERT_EMBD_OR_TOKEN(ubatch); if (ubatch.token) { ggml_tensor * tok_embd_w = layer.nextn.embed_tokens ? layer.nextn.embed_tokens : model.tok_embd; src/models/qwen3next.cpp1 + / 0 −
@@ -653,6 +653,7 @@ llama_model_qwen3next::graph_mtp::graph_mtp(const llama_model & model, const llm // TODO: make static using `ggml_build_forward_select()` // see llm_graph_context::build_inp_embd() for reference ggml_tensor * tok_embd;+ ASSERT_EMBD_OR_TOKEN(ubatch); if (ubatch.token) { ggml_tensor * tok_embd_w = layer.nextn.embed_tokens ? layer.nextn.embed_tokens : model.tok_embd; src/models/qwen4exp.cpp2 + / 1 −
@@ -1240,7 +1240,8 @@ void llm_graph_input_qwen4exp_ple::set_input(const llama_ubatch * ubatch) { ? (llama_token) hparams.ple_image_token_id : (llama_token) hparams.ple_eos_token_id; auto tok_of = [&](int64_t k) -> llama_token {- return ubatch->token ? ubatch->token[k] : img_tok;+ const bool is_embd = !ubatch->token || (ubatch->is_mixed() && ubatch->type[k]);+ return is_embd ? img_tok : ubatch->token[k]; }; const int64_t n_tokens = ubatch->n_tokens;tests/test-batch-alloc.cpp129 + / 0 −
@@ -99,6 +99,15 @@ struct batch_builder { const llama_pos pos[GGML_MROPE_SECTIONS] = { p, 0, 0, 0 }; return add_embd(pos, seq_ids, output); }++ int32_t add_tok(llama_token id, llama_pos p, llama_seq_id seq_id, bool output) {+ const int32_t idx = b.add_token(seq_id);+ GGML_ASSERT(idx >= 0);+ GGML_ASSERT(b.set_token_id(idx, id));+ GGML_ASSERT(b.set_token_pos(idx, &p));+ GGML_ASSERT(b.set_output(idx, output));+ return idx;+ } }; static void test_init(testing & t) {@@ -431,6 +440,125 @@ static void test_content_types(testing & t) { }); } +static void test_mixed(testing & t) {+ llama_vocab vocab;++ t.test("rejected_unless_allowed", [&](testing & t) {+ batch_builder bb(2, nullptr, 4, 1, /*n_vocab*/ 10);+ bb.add_tok(3, 0, 0, false);+ bb.add(1, {0}, true);++ llama_batch_allocr ba_default(1);+ t.assert_true("rejected by default", !ba_default.init(bb.b, vocab, false));++ llama_batch_allocr ba_mixed(1, true);+ t.assert_true("accepted when allowed", ba_mixed.init(bb.b, vocab, false));+ });++ t.test("layout_and_split", [&](testing & t) {+ batch_builder bb(2, nullptr, 4, 1, /*n_vocab*/ 10);+ bb.add_tok(3, 0, 0, false);+ bb.add(1, {0}, false);+ bb.add(2, {0}, false);+ bb.add_tok(5, 3, 0, true);++ llama_batch_allocr ba(1, true);+ t.assert_true(ba.init(bb.b, vocab, false));++ const llama_batch & batch = ba.get_batch();+ t.assert_true(batch.token != nullptr && batch.embd != nullptr);++ const llama_token exp_tok[4] = { 3, 0, 0, 5 };+ const float exp_embd[8] = { 0, 0, 100, 101, 200, 201, 0, 0 };+ for (int i = 0; i < 4; ++i) {+ t.assert_equal(exp_tok[i], batch.token[i]);+ }+ for (int i = 0; i < 8; ++i) {+ t.assert_equal(exp_embd[i], batch.embd[i]);+ }++ llama_ubatch ub0 = ba.split_simple(3);+ t.assert_equal(3u, ub0.n_tokens);+ t.assert_true(ub0.is_mixed());+ const int8_t exp_is_embd[3] = { 0, 1, 1 };+ for (int i = 0; i < 3; ++i) {+ t.assert_equal(exp_is_embd[i], ub0.type[i]);+ t.assert_equal((llama_pos) i, ub0.pos[i]);+ }+ t.assert_equal(3, ub0.token[0]);+ t.assert_equal(0.0f, ub0.embd[0]);+ t.assert_equal(100.0f, ub0.embd[2]);++ // token rows only: a plain token ubatch+ llama_ubatch ub1 = ba.split_simple(3);+ t.assert_equal(1u, ub1.n_tokens);+ t.assert_true(!ub1.is_mixed());+ t.assert_true(ub1.embd == nullptr);+ t.assert_equal(5, ub1.token[0]);+ t.assert_equal((llama_pos) 3, ub1.pos[0]);++ t.assert_equal(0u, ba.split_simple(3).n_tokens);+ });++ t.test("rejects_entry_with_both", [&](testing & t) {+ // token + embd on one entry is the MTP layout+ batch_builder bb(2, nullptr, 4, 1, /*n_vocab*/ 10);+ bb.add_tok(3, 0, 0, false);+ bb.add(1, {0}, false);+ const int32_t idx = bb.add_tok(4, 2, 0, true);+ const auto r = bb.row(idx, bb.n_embd);+ t.assert_true(bb.b.set_token_embd(idx, { r.data(), 1, bb.n_embd }));++ llama_batch_allocr ba(1, true);+ t.assert_true(!ba.init(bb.b, vocab, false));+ });++ t.test("mrope_pos_expanded", [&](testing & t) {+ const uint32_t n_pos = 4;+ batch_builder bb(2, nullptr, 4, n_pos, /*n_vocab*/ 10);++ bb.add_tok(3, 10, 0, false);+ const llama_pos pos1[n_pos] = { 11, 5, 7, 0 };+ bb.add_embd(pos1, {0}, true);++ llama_batch_allocr ba(n_pos, true);+ t.assert_true(ba.init(bb.b, vocab, false));++ llama_ubatch ub = ba.split_simple(2);+ const llama_pos expected[8] = { 10, 11, 10, 5, 10, 7, 0, 0 };+ for (int i = 0; i < 8; ++i) {+ t.assert_equal(expected[i], ub.pos[i]);+ }+ });++ t.test("mrope_rule_follows_first_entry", [&](testing & t) {+ const uint32_t n_pos = 4;++ mock_memory mem;+ mem.ranges[0] = {0, 9};++ llama_batch_allocr ba(n_pos, true);++ // token first: must start after the memory+ {+ batch_builder bb(2, &mem, 4, n_pos, /*n_vocab*/ 10);+ bb.add_tok(3, 9, 0, false);+ const llama_pos pos[n_pos] = { 10, 1, 1, 0 };+ bb.add_embd(pos, {0}, true);+ t.assert_true("token overlapping the memory is rejected", !ba.init(bb.b, vocab, false));+ }++ // embd first: can overlap the memory+ {+ batch_builder bb(2, &mem, 4, n_pos, /*n_vocab*/ 10);+ const llama_pos pos[n_pos] = { 9, 1, 1, 0 };+ bb.add_embd(pos, {0}, false);+ bb.add_tok(3, 10, 0, true);+ t.assert_true("embd overlapping the memory is allowed", ba.init(bb.b, vocab, false));+ }+ });+}+ static void test_split(testing & t) { llama_vocab vocab; @@ -1059,6 +1187,7 @@ int main(int argc, char ** argv) { t.test("init", test_init); t.test("content_types", test_content_types);+ t.test("mixed", test_mixed); t.test("compat", test_compat); t.test("split", test_split); t.test("keep_tail", test_keep_tail);tests/test-llama-archs.cpp88 + / 5 −
@@ -535,6 +535,50 @@ static std::vector<float> get_logits( return ret; } +// entries [n/4, n/2) are embd rows, decoded either as token/embd/token chunks or as one mixed batch+// returns the llama_process() error code+static int32_t get_logits_mixed(+ llama_model * model, llama_context * lctx, const std::vector<llama_token> & tokens, const std::vector<float> & embd, bool mixed,+ std::vector<float> & ret) {+ const uint32_t n_vocab = llama_vocab_n_tokens(llama_model_get_vocab(model));+ const uint32_t n_embd = llama_model_n_embd_inp(model);+ const uint32_t n_tokens = tokens.size();+ const uint32_t i_embd_0 = n_tokens/4;+ const uint32_t i_embd_1 = n_tokens/2;++ const std::vector<uint32_t> bounds = mixed+ ? std::vector<uint32_t>{0, n_tokens}+ : std::vector<uint32_t>{0, i_embd_0, i_embd_1, n_tokens};++ llama_memory_clear(llama_get_memory(lctx), true);+ llama_batch_ext_ptr batch(llama_batch_ext_init(lctx));++ ret.clear();+ ret.reserve(n_tokens*n_vocab);+ for (size_t c = 0; c + 1 < bounds.size(); c++) {+ llama_batch_ext_clear(batch.get());+ for (uint32_t i = bounds[c]; i < bounds[c + 1]; i++) {+ const bool is_embd = i >= i_embd_0 && i < i_embd_1;+ const int32_t idx = is_embd+ ? llama_batch_ext_add_embd(batch.get(), 0, { embd.data() + (size_t) (i - i_embd_0)*n_embd, 1, n_embd })+ : llama_batch_ext_add_token(batch.get(), 0, tokens[i]);+ GGML_ASSERT(idx >= 0);+ const llama_pos pos[4] = { (llama_pos) i, (llama_pos) i, (llama_pos) i, 0 };+ llama_batch_ext_set_pos(batch.get(), idx, pos);+ llama_batch_ext_set_output_logits(batch.get(), idx, true);+ }+ const int32_t err = llama_process(lctx, LLAMA_PROCESS_TYPE_DECODE, batch.get());+ if (err != 0) {+ return err;+ }+ for (uint32_t i = 0; i < bounds[c + 1] - bounds[c]; i++) {+ const float * logits_ith = llama_get_logits_ith(lctx, i);+ ret.insert(ret.end(), logits_ith, logits_ith + n_vocab);+ }+ }+ return 0;+}+ static bool check_causal_attn_toggle( llama_model * model, llama_context * lctx, const std::vector<llama_token> & tokens) { const uint32_t n_vocab = llama_vocab_n_tokens(llama_model_get_vocab(model));@@ -833,15 +877,15 @@ static int test_backends(const std::string & arch_filter, const size_t seed, con max_arch_name_length = std::max(max_arch_name_length, strlen(llm_arch_name(arch))); } - const std::string template_header = std::string("|%" + std::to_string(max_arch_name_length) + "s|%") + std::to_string(max_device_label_length) + "s|%6s|%15s|%9s|\n";+ const std::string template_header = std::string("|%" + std::to_string(max_arch_name_length) + "s|%") + std::to_string(max_device_label_length) + "s|%6s|%15s|%9s|%15s|\n"; const std::string template_row_cfg = std::string("|%" + std::to_string(max_arch_name_length) + "s|%") + std::to_string(max_device_label_length) + "s|%6s|";- const std::string template_row_res = "%15s %10s|%20s|\n";+ const std::string template_row_res = "%15s %10s|%20s|%15s %10s|\n"; bool all_ok = true; size_t n_tests = 0; size_t n_failed = 0; common_log_flush(common_log_main());- LOG(template_header.c_str(), "Model arch.", "Device", "Config", "NMSE vs. CPU", "Roundtrip");+ LOG(template_header.c_str(), "Model arch.", "Device", "Config", "NMSE vs. CPU", "Roundtrip", "Mixed batch"); LOG("|"); for (size_t i = 0; i < max_arch_name_length; i++) { LOG("-");@@ -850,7 +894,7 @@ static int test_backends(const std::string & arch_filter, const size_t seed, con for (size_t i = 0; i < max_device_label_length; i++) { LOG("-"); }- LOG("|------|---------------|---------|\n");+ LOG("|------|---------------|---------|---------------|\n"); for (const llm_arch & arch : llm_arch_all()) { if (arch == LLM_ARCH_UNKNOWN) { continue;@@ -889,7 +933,9 @@ static int test_backends(const std::string & arch_filter, const size_t seed, con std::vector<float> logits_dev; std::string status_nmse = "\033[1;33mSKIP\033[0m"; std::string status_roundtrip = "\033[1;33mSKIP\033[0m";+ std::string status_mixed = "\033[1;33mSKIP\033[0m"; char nmse_str[12] = {0};+ char mixed_str[12] = {0}; bool skip = !arch_supported(arch) || (dc.split_mode == LLAMA_SPLIT_MODE_TENSOR && dc.devs.empty()); bool test_executed = false;@@ -910,6 +956,43 @@ static int test_backends(const std::string & arch_filter, const size_t seed, con test_ok = false; status_nmse = "\033[1;31mFAIL\033[0m"; }++ // chunked decode matches a single batch only with causal attention over a memory+ llama_context * lctx_dev = model_and_ctx_dev.second.get();+ if (!encode && llama_get_memory(lctx_dev) != nullptr) {+ std::vector<float> embd_mixed((size_t) llama_model_n_embd_inp(model_and_ctx_dev.first.get())*tokens.size()/4);+ std::mt19937 gen(seed);+ std::normal_distribution<float> dis(0.0f, stdev);+ for (float & v : embd_mixed) {+ v = dis(gen);+ }+ std::vector<float> logits_mixed;+ std::vector<float> logits_chunks;+ if (llm_arch_supports_mixed_batch(arch)) {+ if (get_logits_mixed(model_and_ctx_dev.first.get(), lctx_dev, tokens, embd_mixed, false, logits_chunks) != 0 ||+ get_logits_mixed(model_and_ctx_dev.first.get(), lctx_dev, tokens, embd_mixed, true, logits_mixed) != 0) {+ throw std::runtime_error("failed to decode mixed batch");+ }+ const double nmse_mixed = nmse(logits_chunks, logits_mixed);+ snprintf(mixed_str, sizeof(mixed_str), "(%.2e)", nmse_mixed);+ status_mixed = "\033[1;32mOK\033[0m";+ if (nmse_mixed > 1e-4) {+ test_ok = false;+ status_mixed = "\033[1;31mFAIL\033[0m";+ }+ } else {+ // must be rejected as an invalid batch, mute the expected error log+ ud.verbosity = LOG_LEVEL_OUTPUT;+ const int32_t err = get_logits_mixed(model_and_ctx_cpu.first.get(), model_and_ctx_cpu.second.get(), tokens, embd_mixed, true, logits_mixed);+ ud.verbosity = verbosity;+ if (err != -1) {+ test_ok = false;+ status_mixed = "\033[1;31mFAIL\033[0m";+ }+ }+ }++ // runs after the mixed batch check, as it leaves the context with non-causal attention if (!encode && !check_causal_attn_toggle(model_and_ctx_dev.first.get(), model_and_ctx_dev.second.get(), tokens)) { if (test_ok) { status_nmse = "\033[1;31mFAIL\033[0m (toggle)";@@ -954,7 +1037,7 @@ static int test_backends(const std::string & arch_filter, const size_t seed, con } // log the results for this test case- LOG(template_row_res.c_str(), status_nmse.c_str(), nmse_str, status_roundtrip.c_str());+ LOG(template_row_res.c_str(), status_nmse.c_str(), nmse_str, status_roundtrip.c_str(), status_mixed.c_str(), mixed_str); } } }