88
99// Ported from executorch/extension/llm/runner/multimodal_prefiller.cpp
1010// with our token-embedding padding fix and LFM2-VL adaptations.
11- //
12- // Supports two PTE shapes, selected from MultimodalDecoderRunner::has_ple()
13- // (auto-detected at load time):
14- // * Legacy : token_embedding -> inputs_embeds;
15- // text_decoder(inputs_embeds, cache_positions).
16- // * PLE : token_embedding -> (inputs_embeds, ple_tok);
17- // text_decoder(inputs_embeds, ple_tok, cache_positions).
1811
1912#include " multimodal_prefiller.h"
2013#include " constants.h"
@@ -49,17 +42,8 @@ MultimodalPrefiller::prefill(const std::vector<MultimodalInput> &inputs,
4942 " prefill: empty input list" );
5043
5144 // ------------------------------------------------------------
52- // Capacity & shape policy from PTE metadata.
53- //
54- // Three knobs drive prefill:
55- // * get_max_seq_len — text_decoder S cap. In dynamic-shape PTEs this
56- // is the per-call chunk size (Gemma4 iter201 =
57- // 128); in static-shape PTEs (LFM2-VL) it is also
58- // the single-shot prefill cap.
59- // * get_max_context_len — total KV budget (Gemma4 iter201 = 2048). Only
60- // materially used by the dynamic-shape path.
61- // * enable_dynamic_shape — selects between chunked (true) and single-shot
62- // padded (false) prefill.
45+ // * get_max_seq_len — text_decoder S cap. Max prefill chunk length (<=get_max_conetxt_len)
46+ // * get_max_context_len — total KV budget. Caps max context length for multi-turn conversation.
6347 // ------------------------------------------------------------
6448 int64_t max_seq_len = -1 ;
6549 {
@@ -92,12 +76,9 @@ MultimodalPrefiller::prefill(const std::vector<MultimodalInput> &inputs,
9276 // ------------------------------------------------------------
9377 // Pass 1: build a fused input_ids buffer spanning all inputs.
9478 //
95- // Mirrors gemma_export/experiments/infer_image.py::prefill_single_shot:
96- // llm_ids = prefix_ids + [0] * num_soft + suffix_ids
97- // Image positions use pad_token_id=0, matching HF modeling_gemma4.py:2190
98- // (placeholder_id is rewritten to 0 before PLE lookup). The decoder embeds
99- // at those positions are then overwritten with the vision encoder output
100- // in pass 2.
79+ // Image positions use pad_token_id (placeholder_id is rewritten to 0 before PLE lookup).
80+ // The decoder embeds at those positions are then
81+ // overwritten with the vision encoder output in pass 2.
10182 // ------------------------------------------------------------
10283 struct ImageSlot {
10384 const MultimodalInput *input; // non-owning, valid for duration of call
@@ -112,12 +93,7 @@ MultimodalPrefiller::prefill(const std::vector<MultimodalInput> &inputs,
11293 // just a TensorImpl*; `Method::get_output(i)` returns `const EValue&` to
11394 // Method-internal storage and Module::execute copies that EValue into the
11495 // returned vector. The copy shares the underlying TensorImpl, so a later
115- // execute() on the same method — a second audio input in this prefill,
116- // a Vulkan backend output-buffer reuse across methods, or a load-time
117- // warm-up — mutates `sizes()` in place under our feet. The original error
118- // ("audio encoder returned 96 tokens, expected 60") is exactly this:
119- // slot.num_audio was captured from the FIRST encode, slot.encoded.size(1)
120- // reflected the SECOND. Mirrors main_mm.cpp:604-675's copy-on-encode.
96+ // execute() on the same method — a second audio input in this prefill would overwrite.
12197 struct AudioSlot {
12298 std::vector<uint8_t > bytes;
12399 ::executorch::aten::ScalarType dtype;
@@ -171,23 +147,22 @@ MultimodalPrefiller::prefill(const std::vector<MultimodalInput> &inputs,
171147 AudioSlot{std::move (bytes), audio_tensor.scalar_type (),
172148 static_cast <int64_t >(ids.size ()), num_audio, audio_hidden});
173149 ids.insert (ids.end (), static_cast <size_t >(num_audio), 0 );
174- } else if (input.is_text () || input.is_tokens ()) {
175- std::vector<uint64_t > tokens;
176- if (input.is_text ()) {
177- auto encode_result = tokenizer_->encode (input.get_text ());
178- if (!encode_result.ok ()) {
179- ET_LOG (Error, " Tokenizer encode error %d" ,
180- static_cast <uint32_t >(encode_result.error ()));
181- return Error::InvalidArgument;
182- }
183- tokens = std::move (*encode_result);
184- } else {
185- tokens = input.get_tokens ();
150+ } else if (input.is_text ()) {
151+ auto encode_result = tokenizer_->encode (input.get_text ());
152+ if (!encode_result.ok ()) {
153+ ET_LOG (Error, " Tokenizer encode error %d" ,
154+ static_cast <uint32_t >(encode_result.error ()));
155+ return Error::InvalidArgument;
186156 }
157+ std::vector<uint64_t > tokens = std::move (*encode_result);
158+ for (auto t : tokens) {
159+ ids.push_back (static_cast <int64_t >(t));
160+ }
161+ } else if (input.is_tokens ()) {
162+ std::vector<uint64_t > tokens = input.get_tokens ();
187163 for (auto t : tokens) {
188164 ids.push_back (static_cast <int64_t >(t));
189165 }
190- } else {
191166 ET_LOG (Error, " Unsupported MultimodalInput type" );
192167 return Error::NotSupported;
193168 }
@@ -383,12 +358,12 @@ MultimodalPrefiller::prefill(const std::vector<MultimodalInput> &inputs,
383358 // ------------------------------------------------------------
384359 // Chunked text_decoder calls.
385360 //
386- // Some PTEs (Gemma4 iter201 ) hard-cap text_decoder's S dim at
361+ // Some PTEs (Gemma4) hard-cap text_decoder's S dim at
387362 // get_max_seq_len (128) while the prefill budget extends to
388363 // get_max_context_len (2048). KV cache state persists across calls via the
389364 // absolute input_pos vector, so chunking is functionally transparent to
390- // the model. For single-shot static-shape PTEs (LFM2-VL) chunk_cap ==
391- // total_len so the loop iterates exactly once — preserving prior behavior.
365+ // the model. For single-shot PTEs chunk_cap == total_len
366+ // so the loop iterates exactly once — preserving prior behavior.
392367 // ------------------------------------------------------------
393368 const int64_t chunk_cap =
394369 decoder_chunk_size > 0 ? decoder_chunk_size : total_len;
0 commit comments