This is an automated email from the ASF dual-hosted git repository.
szaszm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi-minifi-cpp.git
The following commit(s) were added to refs/heads/main by this push:
new 7ee36e14d MINIFICPP-2719 - Add multimodal capability to llama.cpp
processor (#2107)
7ee36e14d is described below
commit 7ee36e14d030d570d8c26f1a13038a974964b8dc
Author: Adam Debreceni <[email protected]>
AuthorDate: Wed May 27 17:24:44 2026 +0200
MINIFICPP-2719 - Add multimodal capability to llama.cpp processor (#2107)
Co-authored-by: Ferenc Gerlits <[email protected]>
---
PROCESSORS.md | 2 +
.../src/minifi_behave/containers/directory.py | 4 +-
.../src/minifi_behave/steps/core_steps.py | 19 +-
cmake/LlamaCpp.cmake | 23 ++-
.../cpp-extension-lib/include/api/core/FlowFile.h | 6 +-
extensions/llamacpp/CMakeLists.txt | 2 +-
.../llamacpp/processors/DefaultLlamaContext.cpp | 191 +++++++++++++++++----
.../llamacpp/processors/DefaultLlamaContext.h | 14 +-
extensions/llamacpp/processors/LlamaContext.h | 3 +-
.../llamacpp/processors/RunLlamaCppInference.cpp | 23 ++-
.../llamacpp/processors/RunLlamaCppInference.h | 16 +-
.../llamacpp/tests/RunLlamaCppInferenceTests.cpp | 90 ++++++++--
extensions/llamacpp/tests/features/environment.py | 6 +-
.../llamacpp/tests/features/llamacpp.feature | 18 +-
.../tests/features/resources/test-image.png | Bin 0 -> 359494 bytes
thirdparty/llamacpp/cpp-23-fixes.patch | 24 ---
thirdparty/llamacpp/lu8_macro_fix.patch | 17 --
thirdparty/llamacpp/mtmd-fix.patch | 44 +++++
18 files changed, 391 insertions(+), 111 deletions(-)
diff --git a/PROCESSORS.md b/PROCESSORS.md
index 9527d7ba5..4c7705b2a 100644
--- a/PROCESSORS.md
+++ b/PROCESSORS.md
@@ -1744,6 +1744,8 @@ In the list below, the names of required properties
appear in bold. Any other pr
| Name | Default Value
| Allowable Values | Description
|
|----------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------|-------------------------------------------------------------------------------------------------------------------------|
| **Model Path** |
| | The filesystem path of the model file in gguf format.
|
+| Output Attribute Name |
| | Specify the attribute to use as output, if not provided,
the content is overridden instead. |
+| MultiModal Model Path |
| | The filesystem path of the multimodal model (visual,
audio) file in gguf format. |
| Temperature | 0.8
| | The temperature to use for sampling.
|
| Top K | 40
| | Limit the next token selection to the K most probable
tokens. Set <= 0 value to use vocab size. |
| Top P | 0.9
| | Limit the next token selection to a subset of tokens with
a cumulative probability above a threshold P. 1.0 = disabled. |
diff --git a/behave_framework/src/minifi_behave/containers/directory.py
b/behave_framework/src/minifi_behave/containers/directory.py
index 50be5a431..47462ef91 100644
--- a/behave_framework/src/minifi_behave/containers/directory.py
+++ b/behave_framework/src/minifi_behave/containers/directory.py
@@ -16,12 +16,12 @@
#
class Directory:
- def __init__(self, path, files: dict[str, str] | None = None, mode="rw"):
+ def __init__(self, path, files: dict[str, str | bytes] | None = None,
mode="rw"):
self.path = path
self.files: dict[str, str] = {}
if files is not None:
self.files = files
self.mode = mode
- def add_file(self, file_name: str, content: str):
+ def add_file(self, file_name: str, content: str | bytes):
self.files[file_name] = content
diff --git a/behave_framework/src/minifi_behave/steps/core_steps.py
b/behave_framework/src/minifi_behave/steps/core_steps.py
index f2efef59b..2c569637b 100644
--- a/behave_framework/src/minifi_behave/steps/core_steps.py
+++ b/behave_framework/src/minifi_behave/steps/core_steps.py
@@ -59,14 +59,15 @@ def create_file_with_size_in_directory(context:
MinifiTestContext, directory: st
dirs.append(new_dir)
-def __add_directory_with_file_to_container(context: MinifiTestContext,
directory: str, file_name: str, content: str, container_name: str):
+def __add_directory_with_file_to_container(context: MinifiTestContext,
directory: str, file_name: str, content: str | bytes, container_name: str):
dirs = context.get_or_create_minifi_container(container_name).dirs
- new_content = content.replace("\\n", "\n")
+ if isinstance(content, str):
+ content = content.replace("\\n", "\n")
if directory in dirs:
- dirs[directory].files[file_name] = new_content
+ dirs[directory].files[file_name] = content
return
new_dir = Directory(directory)
- new_dir.files[file_name] = new_content
+ new_dir.files[file_name] = content
dirs.append(new_dir)
@@ -82,6 +83,16 @@ def create_file_with_content_in_directory(context:
MinifiTestContext, directory:
context.execute_steps(f'given a directory at "{directory}" has a file with
the content "{content}" in the "{DEFAULT_MINIFI_CONTAINER_NAME}" flow')
+@step('a directory at "{directory}" has a file with the content from "{path}"')
+@step("a directory at '{directory}' has a file with the content from '{path}'")
+def create_file_with_content_from_path_in_directory(context:
MinifiTestContext, directory: str, path: str):
+ assert context.resource_dir is not None, "Cannot copy file if resource_dir
is not set for the context"
+ content = None
+ with open(context.resource_dir / path, "rb") as f:
+ content = f.read()
+ __add_directory_with_file_to_container(context, directory,
str(uuid.uuid4()), content, DEFAULT_MINIFI_CONTAINER_NAME)
+
+
@step('a directory at "{directory}" has a file "{file_name}" with the content
"{content}"')
def create_file_with_name_and_content_in_directory(context: MinifiTestContext,
directory: str, file_name: str, content: str):
__add_directory_with_file_to_container(context, directory, file_name,
content, DEFAULT_MINIFI_CONTAINER_NAME)
diff --git a/cmake/LlamaCpp.cmake b/cmake/LlamaCpp.cmake
index f78101c6f..d9e764379 100644
--- a/cmake/LlamaCpp.cmake
+++ b/cmake/LlamaCpp.cmake
@@ -21,6 +21,7 @@ set(BUILD_SHARED_LIBS "OFF" CACHE STRING "" FORCE)
set(LLAMA_BUILD_TESTS "OFF" CACHE STRING "" FORCE)
set(LLAMA_BUILD_EXAMPLES "OFF" CACHE STRING "" FORCE)
set(LLAMA_BUILD_SERVER "OFF" CACHE STRING "" FORCE)
+set(LLAMA_BUILD_COMMON "ON" CACHE STRING "" FORCE)
set(GGML_OPENMP "OFF" CACHE STRING "" FORCE)
set(GGML_METAL "OFF" CACHE STRING "" FORCE)
set(GGML_BLAS "OFF" CACHE STRING "" FORCE)
@@ -30,24 +31,30 @@ else()
set(GGML_NATIVE "ON" CACHE STRING "" FORCE)
endif()
-set(PATCH_FILE_1
"${CMAKE_SOURCE_DIR}/thirdparty/llamacpp/lu8_macro_fix.patch") #
https://github.com/ggml-org/llama.cpp/issues/12740
-set(PATCH_FILE_2 "${CMAKE_SOURCE_DIR}/thirdparty/llamacpp/cpp-23-fixes.patch")
+set(PATCH_FILE_1 "${CMAKE_SOURCE_DIR}/thirdparty/llamacpp/mtmd-fix.patch")
set(PC ${Bash_EXECUTABLE} -c "set -x &&\
- (\\\"${Patch_EXECUTABLE}\\\" -p1 -R -s -f --dry-run -i
\\\"${PATCH_FILE_1}\\\" || \\\"${Patch_EXECUTABLE}\\\" -p1 -N -i
\\\"${PATCH_FILE_1}\\\") &&\
- (\\\"${Patch_EXECUTABLE}\\\" -p1 -R -s -f --dry-run -i
\\\"${PATCH_FILE_2}\\\" || \\\"${Patch_EXECUTABLE}\\\" -p1 -N -i
\\\"${PATCH_FILE_2}\\\")")
+ (\\\"${Patch_EXECUTABLE}\\\" -p1 -R -s -f --dry-run -i
\\\"${PATCH_FILE_1}\\\" || \\\"${Patch_EXECUTABLE}\\\" -p1 -N -i
\\\"${PATCH_FILE_1}\\\")")
+
FetchContent_Declare(llamacpp
- URL
https://github.com/ggml-org/llama.cpp/archive/refs/tags/b5958.tar.gz
- URL_HASH
SHA256=4e8a2abd83092aa446cd13556f6fe8777139da7b191bdaa0e1b79fe9740b36a6
- PATCH_COMMAND "${PC}"
- SYSTEM
+ URL https://github.com/ggml-org/llama.cpp/archive/refs/tags/b8944.tar.gz
+ URL_HASH
SHA256=ca231c8aca086f56bad3ed371f6dc5b01e971e812a8ddf67564f087390c0e781
+ PATCH_COMMAND "${PC}"
+ SYSTEM
)
FetchContent_MakeAvailable(llamacpp)
+if(MSVC AND TARGET llama)
+ target_compile_options(llama PRIVATE /Zc:__cplusplus)
+endif()
+
set(LLAMACPP_INCLUDE_DIRS
"${llamacpp_SOURCE_DIR}/include"
"${llamacpp_SOURCE_DIR}/ggml/include"
+ "${llamacpp_SOURCE_DIR}/tools"
+ "${llamacpp_SOURCE_DIR}/common"
+ "${llamacpp_SOURCE_DIR}/vendor"
CACHE STRING "" FORCE
)
diff --git a/extension-framework/cpp-extension-lib/include/api/core/FlowFile.h
b/extension-framework/cpp-extension-lib/include/api/core/FlowFile.h
index 833dfdaf7..4a288f36d 100644
--- a/extension-framework/cpp-extension-lib/include/api/core/FlowFile.h
+++ b/extension-framework/cpp-extension-lib/include/api/core/FlowFile.h
@@ -26,7 +26,11 @@ namespace org::apache::nifi::minifi::api::core {
struct EnsureMovedFromDeleter {
void operator()(MinifiFlowFile* ff) {
if (ff) {
- throw std::logic_error("Each flowfile should be either transferred or
removed");
+ if (std::uncaught_exceptions()) {
+ // there is already an exception in progress, do not terminate the
process (although there are scenarios we could throw here)
+ } else {
+ throw std::logic_error("Each flowfile should be either transferred or
removed");
+ }
}
}
};
diff --git a/extensions/llamacpp/CMakeLists.txt
b/extensions/llamacpp/CMakeLists.txt
index 421143f69..1de6be458 100644
--- a/extensions/llamacpp/CMakeLists.txt
+++ b/extensions/llamacpp/CMakeLists.txt
@@ -31,7 +31,7 @@ add_minifi_library(minifi-llamacpp SHARED ${SOURCES})
target_include_directories(minifi-llamacpp PUBLIC
"${CMAKE_SOURCE_DIR}/extensions/llamacpp")
target_include_directories(minifi-llamacpp PUBLIC "${LLAMACPP_INCLUDE_DIRS}")
-target_link_libraries(minifi-llamacpp minifi-cpp-extension-lib llama)
+target_link_libraries(minifi-llamacpp minifi-cpp-extension-lib llama mtmd
llama-common)
register_c_api_extension(minifi-llamacpp "LLAMACPP EXTENSION"
LLAMACPP-EXTENSION "Provides llama.cpp support" "extensions/llamacpp/tests")
diff --git a/extensions/llamacpp/processors/DefaultLlamaContext.cpp
b/extensions/llamacpp/processors/DefaultLlamaContext.cpp
index 23559fb7c..e1752dfcb 100644
--- a/extensions/llamacpp/processors/DefaultLlamaContext.cpp
+++ b/extensions/llamacpp/processors/DefaultLlamaContext.cpp
@@ -16,8 +16,12 @@
*/
#include "DefaultLlamaContext.h"
+
+#include <range/v3/all.hpp>
+
#include "minifi-cpp/Exception.h"
#include "fmt/format.h"
+#include "mtmd/mtmd-helper.h"
namespace org::apache::nifi::minifi::extensions::llamacpp::processors {
@@ -36,17 +40,18 @@ std::vector<llama_token> tokenizeInput(const llama_vocab*
vocab, const std::stri
return tokenized_input;
}
-constexpr size_t DEFAULT_BUFFER_SIZE = 4096;
-
} // namespace
-DefaultLlamaContext::DefaultLlamaContext(const std::filesystem::path&
model_path, const LlamaSamplerParams& llama_sampler_params, const
LlamaContextParams& llama_ctx_params) {
+DefaultLlamaContext::DefaultLlamaContext(const std::filesystem::path&
model_path, const std::optional<std::filesystem::path>& multimodal_model_path,
+ const LlamaSamplerParams& llama_sampler_params, const LlamaContextParams&
llama_ctx_params, const std::shared_ptr<core::logging::Logger>& logger) {
llama_model_ = llama_model_load_from_file(model_path.string().c_str(),
llama_model_default_params()); //
NOLINT(cppcoreguidelines-prefer-member-initializer)
if (!llama_model_) {
throw Exception(ExceptionType::PROCESS_SCHEDULE_EXCEPTION,
fmt::format("Failed to load model from '{}'", model_path.string()));
}
+ chat_template_ = common_chat_templates_init(llama_model_, "");
+
llama_context_params ctx_params = llama_context_default_params();
ctx_params.n_ctx = llama_ctx_params.n_ctx;
ctx_params.n_batch = llama_ctx_params.n_batch;
@@ -54,7 +59,7 @@ DefaultLlamaContext::DefaultLlamaContext(const
std::filesystem::path& model_path
ctx_params.n_seq_max = llama_ctx_params.n_seq_max;
ctx_params.n_threads = llama_ctx_params.n_threads;
ctx_params.n_threads_batch = llama_ctx_params.n_threads_batch;
- ctx_params.flash_attn = false;
+ ctx_params.flash_attn_type = LLAMA_FLASH_ATTN_TYPE_DISABLED;
llama_ctx_ = llama_init_from_model(llama_model_, ctx_params);
auto sparams = llama_sampler_chain_default_params();
@@ -73,9 +78,30 @@ DefaultLlamaContext::DefaultLlamaContext(const
std::filesystem::path& model_path
llama_sampler_chain_add(llama_sampler_,
llama_sampler_init_temp(*llama_sampler_params.temperature));
}
llama_sampler_chain_add(llama_sampler_,
llama_sampler_init_dist(LLAMA_DEFAULT_SEED));
+
+ if (multimodal_model_path) {
+ initializeMultimodalContext(multimodal_model_path.value());
+ logger->log_info("Successfully loaded multimodal model from '{}'",
multimodal_model_path->string());
+ } else {
+ logger->log_info("No multimodal model path provided");
+ }
+}
+
+void DefaultLlamaContext::initializeMultimodalContext(const
std::filesystem::path& multimodal_model_path) {
+ mtmd_context_params mparams = mtmd_context_params_default();
+ mparams.use_gpu = false;
+ mparams.flash_attn_type = LLAMA_FLASH_ATTN_TYPE_DISABLED;
+
+ multimodal_ctx_ =
mtmd_init_from_file(multimodal_model_path.string().c_str(), llama_model_,
mparams);
+ if (!multimodal_ctx_) {
+ throw Exception(ExceptionType::PROCESS_SCHEDULE_EXCEPTION,
fmt::format("Failed to load multimodal model from '{}'",
multimodal_model_path.string()));
+ }
}
+
DefaultLlamaContext::~DefaultLlamaContext() {
+ mtmd_free(multimodal_ctx_);
+ multimodal_ctx_ = nullptr;
llama_sampler_free(llama_sampler_);
llama_sampler_ = nullptr;
llama_free(llama_ctx_);
@@ -85,47 +111,132 @@ DefaultLlamaContext::~DefaultLlamaContext() {
}
std::optional<std::string> DefaultLlamaContext::applyTemplate(const
std::vector<LlamaChatMessage>& messages) {
- std::vector<llama_chat_message> llama_messages;
- llama_messages.reserve(messages.size());
- std::transform(messages.begin(), messages.end(),
std::back_inserter(llama_messages),
- [](const LlamaChatMessage& msg) { return
llama_chat_message{.role = msg.role.c_str(), .content = msg.content.c_str()};
});
- std::string text;
- text.resize(DEFAULT_BUFFER_SIZE);
- const char * chat_template = llama_model_chat_template(llama_model_,
nullptr);
- int32_t res_size = llama_chat_apply_template(chat_template,
llama_messages.data(), llama_messages.size(), true, text.data(),
gsl::narrow<int32_t>(text.size()));
- if (res_size < 0) {
+ if (!chat_template_) {
return std::nullopt;
}
- if (res_size > gsl::narrow<int32_t>(text.size())) {
- text.resize(res_size);
- res_size = llama_chat_apply_template(chat_template, llama_messages.data(),
llama_messages.size(), true, text.data(), gsl::narrow<int32_t>(text.size()));
- if (res_size < 0) {
- return std::nullopt;
+ common_chat_templates_inputs inputs;
+ for (auto& msg : messages) {
+ common_chat_msg chat_msg;
+ chat_msg.role = msg.role;
+ chat_msg.content = msg.content;
+ inputs.messages.push_back(std::move(chat_msg));
+ }
+ inputs.enable_thinking = false; // TODO(adebreceni): MINIFICPP-2800
common_chat_templates_support_enable_thinking(chat_template_.get());
+
+ return common_chat_templates_apply(chat_template_.get(), inputs).prompt;
+}
+
+namespace {
+
+struct mtmd_bitmap_deleter {
+ void operator()(mtmd_bitmap* val) { mtmd_bitmap_free(val); }
+};
+using unique_bitmap_ptr = std::unique_ptr<mtmd_bitmap, mtmd_bitmap_deleter>;
+
+struct mtmd_input_chunks_deleter {
+ void operator()(mtmd_input_chunks* val) { mtmd_input_chunks_free(val); }
+};
+using unique_mtmd_input_chunks_ptr = std::unique_ptr<mtmd_input_chunks,
mtmd_input_chunks_deleter>;
+
+class unique_llama_batch {
+ public:
+ unique_llama_batch() {
+ batch_.n_tokens = 1;
+ batch_.n_seq_id[0] = 1;
+ batch_.seq_id[0][0] = 0;
+ batch_.logits[0] = true;
+ }
+ unique_llama_batch(const unique_llama_batch&) = delete;
+ unique_llama_batch(unique_llama_batch&&) = delete;
+ unique_llama_batch& operator=(const unique_llama_batch&) = delete;
+ unique_llama_batch& operator=(unique_llama_batch&&) = delete;
+
+ ~unique_llama_batch() {
+ llama_batch_free(batch_);
+ }
+
+ llama_batch* operator->() {
+ return &batch_;
+ }
+
+ llama_batch& operator*() {
+ return batch_;
+ }
+
+ private:
+ llama_batch batch_{llama_batch_init(1, 0, 1)};
+};
+
+} // namespace
+
+std::expected<llama_pos, std::string>
DefaultLlamaContext::tokenizeAndDecodeMultimodalInput(const std::string&
prompt, const std::vector<std::vector<std::byte>>& files) {
+ if (files.empty()) {
+ return std::unexpected{"Multimodal input requires at least one file"};
+ }
+ std::vector<unique_bitmap_ptr> bitmaps;
+ for (auto& file : files) {
+ unique_bitmap_ptr bitmap{mtmd_helper_bitmap_init_from_buf(multimodal_ctx_,
reinterpret_cast<const unsigned char*>(file.data()), file.size())};
+ if (!bitmap) {
+ throw Exception(PROCESSOR_EXCEPTION, "Failed to create multimodal bitmap
from buffer");
}
+ bitmaps.push_back(std::move(bitmap));
+ }
+ mtmd_input_text inp_txt = {
+ .text = prompt.c_str(),
+ .add_special = true,
+ .parse_special = true,
+ };
+ unique_mtmd_input_chunks_ptr chunks{mtmd_input_chunks_init()};
+ auto bitmap_c_ptrs = bitmaps | ranges::views::transform([] (auto& ptr)
{return static_cast<const mtmd_bitmap*>(ptr.get());}) |
ranges::to<std::vector>();
+ auto tokenized = mtmd_tokenize(multimodal_ctx_, chunks.get(), &inp_txt,
bitmap_c_ptrs.data(), bitmap_c_ptrs.size());
+ if (tokenized != 0) {
+ throw Exception(PROCESSOR_EXCEPTION, fmt::format("Failed to tokenize
multimodal prompt, error: {}", tokenized));
+ }
+ llama_pos n_past = 0;
+ // this automatically calls llama_decode
+ auto status = mtmd_helper_eval_chunks(multimodal_ctx_, llama_ctx_,
chunks.get(), 0, 0, 1, true, &n_past);
+ if (status != 0) {
+ throw Exception(PROCESSOR_EXCEPTION, fmt::format("Failed to eval
multimodal chunks, error: {}", status));
}
- text.resize(res_size);
- return text;
+ return n_past;
}
-std::expected<GenerationResult, std::string>
DefaultLlamaContext::generate(const std::string& input,
std::function<void(std::string_view/*token*/)> token_handler) {
+std::expected<GenerationResult, std::string>
DefaultLlamaContext::generate(const std::string& prompt, const
std::vector<std::vector<std::byte>>& files,
+ std::function<void(std::string_view/*token*/)> token_handler) {
GenerationResult result{};
auto start_time = std::chrono::steady_clock::now();
+ llama_memory_seq_rm(llama_get_memory(llama_ctx_), 0, -1, -1);
const llama_vocab * vocab = llama_model_get_vocab(llama_model_);
- std::vector<llama_token> tokenized_input = tokenizeInput(vocab, input);
- result.num_tokens_in = gsl::narrow<uint64_t>(tokenized_input.size());
+ llama_pos n_past = 0;
+ std::vector<llama_token> tokenized_input;
+ int32_t decode_status = 0;
+ if (multimodal_ctx_) {
+ auto input_size = tokenizeAndDecodeMultimodalInput(prompt, files);
+ if (!input_size) {
+ return std::unexpected{input_size.error()};
+ }
+ n_past = input_size.value();
+ } else {
+ if (!files.empty()) {
+ return std::unexpected{"Model is not configured for multimodal input"};
+ }
+ try {
+ tokenized_input = tokenizeInput(vocab, prompt);
+ } catch (std::exception& e) {
+ return std::unexpected{fmt::format("Error during tokenization: {}",
e.what())};
+ } catch (...) {
+ return std::unexpected{"Unknown error during tokenization"};
+ }
+ n_past = gsl::narrow<llama_pos>(tokenized_input.size());
+ decode_status = llama_decode(llama_ctx_,
llama_batch_get_one(tokenized_input.data(), n_past));
+ }
+ result.num_tokens_in = gsl::narrow<uint64_t>(n_past);
- llama_batch batch = llama_batch_get_one(tokenized_input.data(),
gsl::narrow<int32_t>(tokenized_input.size()));
llama_token new_token_id = 0;
bool first_token_generated = false;
- while (true) {
- int32_t res = llama_decode(llama_ctx_, batch);
- if (res == 1) {
- return std::unexpected{"Could not find a KV slot for the batch (try
reducing the size of the batch or increase the context)"};
- } else if (res < 0) {
- return std::unexpected{"Error occurred while executing llama decode"};
- }
-
+ unique_llama_batch batch;
+ while (decode_status == 0) {
new_token_id = llama_sampler_sample(llama_sampler_, llama_ctx_, -1);
if (!first_token_generated) {
result.time_to_first_token =
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now()
- start_time);
@@ -147,8 +258,22 @@ std::expected<GenerationResult, std::string>
DefaultLlamaContext::generate(const
gsl_Assert(len < 128);
std::string_view token_str{buf.data(),
gsl::narrow<std::string_view::size_type>(len)};
- batch = llama_batch_get_one(&new_token_id, 1);
+ batch->token[0] = new_token_id;
+ batch->pos[0] = n_past;
+ ++n_past;
token_handler(token_str);
+
+ decode_status = llama_decode(llama_ctx_, *batch);
+ }
+
+ if (decode_status == 1) {
+ return std::unexpected("Could not find a KV slot for the batch (try
reducing the size of the batch or increase the context)");
+ }
+ if (decode_status == 2) {
+ return std::unexpected("Llama decode aborted");
+ }
+ if (decode_status < 0) {
+ return std::unexpected("Error occurred while executing llama decode");
}
result.tokens_per_second =
diff --git a/extensions/llamacpp/processors/DefaultLlamaContext.h
b/extensions/llamacpp/processors/DefaultLlamaContext.h
index 94c3346b2..1994a3e1c 100644
--- a/extensions/llamacpp/processors/DefaultLlamaContext.h
+++ b/extensions/llamacpp/processors/DefaultLlamaContext.h
@@ -19,12 +19,16 @@
#include "LlamaContext.h"
#include "llama.h"
#include "LlamaBackendInitializer.h"
+#include "chat.h"
+#include "mtmd/mtmd.h"
+#include "minifi-cpp/core/logging/Logger.h"
namespace org::apache::nifi::minifi::extensions::llamacpp::processors {
class DefaultLlamaContext : public LlamaContext {
public:
- DefaultLlamaContext(const std::filesystem::path& model_path, const
LlamaSamplerParams& llama_sampler_params, const LlamaContextParams&
llama_ctx_params);
+ DefaultLlamaContext(const std::filesystem::path& model_path, const
std::optional<std::filesystem::path>& multimodal_model_path,
+ const LlamaSamplerParams& llama_sampler_params, const
LlamaContextParams& llama_ctx_params, const
std::shared_ptr<core::logging::Logger>& logger);
DefaultLlamaContext(const DefaultLlamaContext&) = delete;
DefaultLlamaContext(DefaultLlamaContext&&) = delete;
DefaultLlamaContext& operator=(const DefaultLlamaContext&) = delete;
@@ -32,12 +36,18 @@ class DefaultLlamaContext : public LlamaContext {
~DefaultLlamaContext() override;
std::optional<std::string> applyTemplate(const
std::vector<LlamaChatMessage>& messages) override;
- std::expected<GenerationResult, std::string> generate(const std::string&
input, std::function<void(std::string_view/*token*/)> token_handler) override;
+ std::expected<GenerationResult, std::string> generate(const std::string&
prompt, const std::vector<std::vector<std::byte>>& files,
+ std::function<void(std::string_view/*token*/)> token_handler) override;
private:
+ void initializeMultimodalContext(const std::filesystem::path&
multimodal_model_path);
+ std::expected<llama_pos, std::string> tokenizeAndDecodeMultimodalInput(const
std::string& prompt, const std::vector<std::vector<std::byte>>& files);
+
const LlamaBackendInitializer& llama_context_initializer_ =
LlamaBackendInitializer::get();
llama_model* llama_model_{};
+ common_chat_templates_ptr chat_template_;
llama_context* llama_ctx_{};
+ mtmd_context* multimodal_ctx_{};
llama_sampler* llama_sampler_{};
};
diff --git a/extensions/llamacpp/processors/LlamaContext.h
b/extensions/llamacpp/processors/LlamaContext.h
index a7cd2eb44..557b27026 100644
--- a/extensions/llamacpp/processors/LlamaContext.h
+++ b/extensions/llamacpp/processors/LlamaContext.h
@@ -59,7 +59,8 @@ struct GenerationResult {
class LlamaContext {
public:
virtual std::optional<std::string> applyTemplate(const
std::vector<LlamaChatMessage>& messages) = 0;
- virtual std::expected<GenerationResult, std::string> generate(const
std::string& input, std::function<void(std::string_view/*token*/)>
token_handler) = 0;
+ virtual std::expected<GenerationResult, std::string> generate(const
std::string& input, const std::vector<std::vector<std::byte>>& files,
+ std::function<void(std::string_view/*token*/)> token_handler) = 0;
virtual ~LlamaContext() = default;
};
diff --git a/extensions/llamacpp/processors/RunLlamaCppInference.cpp
b/extensions/llamacpp/processors/RunLlamaCppInference.cpp
index 5927a0199..ede77cd06 100644
--- a/extensions/llamacpp/processors/RunLlamaCppInference.cpp
+++ b/extensions/llamacpp/processors/RunLlamaCppInference.cpp
@@ -31,7 +31,9 @@ namespace
org::apache::nifi::minifi::extensions::llamacpp::processors {
MinifiStatus RunLlamaCppInference::onScheduleImpl(api::core::ProcessContext&
context) {
model_path_.clear();
model_path_ = api::utils::parseProperty(context, ModelPath);
+ multimodal_model_path_ = api::utils::parseOptionalProperty(context,
MultiModalModelPath);
system_prompt_ = context.getProperty(SystemPrompt).value_or("");
+ output_attribute_ = api::utils::parseOptionalProperty(context,
OutputAttributeName);
LlamaSamplerParams llama_sampler_params;
llama_sampler_params.temperature =
api::utils::parseOptionalFloatProperty(context, Temperature);
@@ -51,9 +53,9 @@ MinifiStatus
RunLlamaCppInference::onScheduleImpl(api::core::ProcessContext& con
llama_ctx_params.n_threads_batch =
gsl::narrow<int32_t>(api::utils::parseI64Property(context,
ThreadsForBatchProcessing));
if (llama_context_provider_) {
- llama_ctx_ = llama_context_provider_(model_path_, llama_sampler_params,
llama_ctx_params);
+ llama_ctx_ = llama_context_provider_(model_path_, multimodal_model_path_,
llama_sampler_params, llama_ctx_params);
} else {
- llama_ctx_ = std::make_unique<DefaultLlamaContext>(model_path_,
llama_sampler_params, llama_ctx_params);
+ llama_ctx_ = std::make_unique<DefaultLlamaContext>(model_path_,
multimodal_model_path_, llama_sampler_params, llama_ctx_params, logger_);
}
return MINIFI_STATUS_SUCCESS;
@@ -76,10 +78,16 @@ MinifiStatus
RunLlamaCppInference::onTriggerImpl(api::core::ProcessContext& cont
auto prompt = context.getProperty(Prompt, &flow_file).value_or("");
auto read_result = session.readBuffer(flow_file);
+ std::vector<std::vector<std::byte>> files;
std::string input_data_and_prompt;
if (!read_result.empty()) {
input_data_and_prompt.append("Input data (or flow file content):\n");
- input_data_and_prompt.append({reinterpret_cast<const
char*>(read_result.data()), read_result.size()});
+ if (multimodal_model_path_) {
+ input_data_and_prompt.append(mtmd_default_marker());
+ files.push_back(std::move(read_result));
+ } else {
+ input_data_and_prompt.append({reinterpret_cast<const
char*>(read_result.data()), read_result.size()});
+ }
input_data_and_prompt.append("\n\n");
}
input_data_and_prompt.append(prompt);
@@ -111,7 +119,7 @@ MinifiStatus
RunLlamaCppInference::onTriggerImpl(api::core::ProcessContext& cont
auto start_time = std::chrono::steady_clock::now();
std::string text;
- auto generation_result = llama_ctx_->generate(*input, [&] (std::string_view
token) {
+ auto generation_result = llama_ctx_->generate(*input, files, [&]
(std::string_view token) {
text += token;
});
@@ -133,7 +141,12 @@ MinifiStatus
RunLlamaCppInference::onTriggerImpl(api::core::ProcessContext& cont
session.setAttribute(flow_file, LlamaCppTimeToFirstToken.name,
std::to_string(generation_result->time_to_first_token.count()) + " ms");
session.setAttribute(flow_file, LlamaCppTokensPerSecond.name,
fmt::format("{:.2f}", generation_result->tokens_per_second));
- session.writeBuffer(flow_file, text);
+ if (output_attribute_) {
+ session.setAttribute(flow_file, output_attribute_.value(), text);
+ } else {
+ session.writeBuffer(flow_file, text);
+ }
+
session.transfer(std::move(flow_file), Success);
return MINIFI_STATUS_SUCCESS;
diff --git a/extensions/llamacpp/processors/RunLlamaCppInference.h
b/extensions/llamacpp/processors/RunLlamaCppInference.h
index 76ae3a1f6..5634df4de 100644
--- a/extensions/llamacpp/processors/RunLlamaCppInference.h
+++ b/extensions/llamacpp/processors/RunLlamaCppInference.h
@@ -29,7 +29,8 @@
namespace org::apache::nifi::minifi::extensions::llamacpp::processors {
using LlamaContextProvider =
- std::function<std::unique_ptr<LlamaContext>(const std::filesystem::path&
model_path, const LlamaSamplerParams& llama_sampler_params, const
LlamaContextParams& llama_ctx_params)>;
+ std::function<std::unique_ptr<LlamaContext>(const std::filesystem::path&
model_path, const std::optional<std::filesystem::path>& multimodal_model_path,
+ const LlamaSamplerParams& llama_sampler_params, const
LlamaContextParams& llama_ctx_params)>;
class RunLlamaCppInferenceMetrics {
public:
@@ -58,6 +59,15 @@ class RunLlamaCppInference : public api::core::ProcessorImpl
{
EXTENSIONAPI static constexpr auto ModelPath =
core::PropertyDefinitionBuilder<>::createProperty("Model Path")
.withDescription("The filesystem path of the model file in gguf format.")
.isRequired(true)
+ .withValidator(core::StandardPropertyValidators::NON_BLANK_VALIDATOR)
+ .build();
+ EXTENSIONAPI static constexpr auto OutputAttributeName =
core::PropertyDefinitionBuilder<>::createProperty("Output Attribute Name")
+ .withDescription("Specify the attribute to use as output, if not
provided, the content is overridden instead.")
+ .withValidator(core::StandardPropertyValidators::NON_BLANK_VALIDATOR)
+ .build();
+ EXTENSIONAPI static constexpr auto MultiModalModelPath =
core::PropertyDefinitionBuilder<>::createProperty("MultiModal Model Path")
+ .withDescription("The filesystem path of the multimodal model (visual,
audio) file in gguf format.")
+ .withValidator(core::StandardPropertyValidators::NON_BLANK_VALIDATOR)
.build();
EXTENSIONAPI static constexpr auto Temperature =
core::PropertyDefinitionBuilder<>::createProperty("Temperature")
.withDescription("The temperature to use for sampling.")
@@ -128,6 +138,8 @@ class RunLlamaCppInference : public
api::core::ProcessorImpl {
EXTENSIONAPI static constexpr auto Properties =
std::to_array<core::PropertyReference>({
ModelPath,
+ OutputAttributeName,
+ MultiModalModelPath,
Temperature,
TopK,
TopP,
@@ -167,7 +179,9 @@ class RunLlamaCppInference : public
api::core::ProcessorImpl {
void increaseTokensOut(uint64_t token_count);
std::string model_path_;
+ std::optional<std::string> multimodal_model_path_;
std::string system_prompt_;
+ std::optional<std::string> output_attribute_;
LlamaContextProvider llama_context_provider_;
std::unique_ptr<LlamaContext> llama_ctx_;
diff --git a/extensions/llamacpp/tests/RunLlamaCppInferenceTests.cpp
b/extensions/llamacpp/tests/RunLlamaCppInferenceTests.cpp
index 9fa893fba..3582c2190 100644
--- a/extensions/llamacpp/tests/RunLlamaCppInferenceTests.cpp
+++ b/extensions/llamacpp/tests/RunLlamaCppInferenceTests.cpp
@@ -37,10 +37,14 @@ class MockLlamaContext : public processors::LlamaContext {
return "Test input";
}
- std::expected<processors::GenerationResult, std::string> generate(const
std::string& input, std::function<void(std::string_view/*token*/)>
token_handler) override {
+ std::expected<processors::GenerationResult, std::string> generate(const
std::string& input, const std::vector<std::vector<std::byte>>& files,
+ std::function<void(std::string_view/*token*/)> token_handler) override
{
if (fail_generation_) {
return std::unexpected{"Generation failed"};
}
+ if (multimodal_ && files.empty()) {
+ return std::unexpected{"Files empty"};
+ }
processors::GenerationResult result;
input_ = input;
token_handler("Test ");
@@ -69,7 +73,12 @@ class MockLlamaContext : public processors::LlamaContext {
fail_apply_template_ = true;
}
+ void setMultimodal() {
+ multimodal_ = true;
+ }
+
private:
+ bool multimodal_{false};
bool fail_generation_{false};
bool fail_apply_template_{false};
std::vector<processors::LlamaChatMessage> messages_;
@@ -84,7 +93,8 @@ TEST_CASE("Prompt is generated correctly with default
parameters") {
processors::LlamaContextParams test_context_params;
minifi::test::SingleProcessorTestController
controller(minifi::test::utils::make_custom_c_processor<processors::RunLlamaCppInference>(
core::ProcessorMetadata{utils::Identifier{}, "RunLlamaCppInference",
logging::LoggerFactory<processors::RunLlamaCppInference>::getLogger()},
- [&](const std::filesystem::path& model_path, const
processors::LlamaSamplerParams& sampler_params, const
processors::LlamaContextParams& context_params) {
+ [&](const std::filesystem::path& model_path, const
std::optional<std::filesystem::path>&, const processors::LlamaSamplerParams&
sampler_params,
+ const processors::LlamaContextParams& context_params) {
test_model_path = model_path;
test_sampler_params = sampler_params;
test_context_params = context_params;
@@ -130,7 +140,8 @@ TEST_CASE("Prompt is generated correctly with custom
parameters") {
processors::LlamaContextParams test_context_params;
minifi::test::SingleProcessorTestController
controller(minifi::test::utils::make_custom_c_processor<processors::RunLlamaCppInference>(
core::ProcessorMetadata{utils::Identifier{}, "RunLlamaCppInference",
logging::LoggerFactory<processors::RunLlamaCppInference>::getLogger()},
- [&](const std::filesystem::path& model_path, const
processors::LlamaSamplerParams& sampler_params, const
processors::LlamaContextParams& context_params) {
+ [&](const std::filesystem::path& model_path, const
std::optional<std::filesystem::path>&, const processors::LlamaSamplerParams&
sampler_params,
+ const processors::LlamaContextParams& context_params) {
test_model_path = model_path;
test_sampler_params = sampler_params;
test_context_params = context_params;
@@ -182,7 +193,7 @@ TEST_CASE("Empty flow file does not include input data in
prompt") {
auto mock_llama_context_ptr = mock_llama_context.get();
minifi::test::SingleProcessorTestController
controller(minifi::test::utils::make_custom_c_processor<processors::RunLlamaCppInference>(
core::ProcessorMetadata{utils::Identifier{}, "RunLlamaCppInference",
logging::LoggerFactory<processors::RunLlamaCppInference>::getLogger()},
- [&](const std::filesystem::path&, const processors::LlamaSamplerParams&,
const processors::LlamaContextParams&) {
+ [&](const std::filesystem::path&, const
std::optional<std::filesystem::path>&, const processors::LlamaSamplerParams&,
const processors::LlamaContextParams&) {
return std::move(mock_llama_context);
}));
LogTestController::getInstance().setTrace<processors::RunLlamaCppInference>();
@@ -206,7 +217,7 @@ TEST_CASE("Empty flow file does not include input data in
prompt") {
TEST_CASE("Invalid values for optional double type properties throw
exception") {
minifi::test::SingleProcessorTestController
controller(minifi::test::utils::make_custom_c_processor<processors::RunLlamaCppInference>(
core::ProcessorMetadata{utils::Identifier{}, "RunLlamaCppInference",
logging::LoggerFactory<processors::RunLlamaCppInference>::getLogger()},
- [&](const std::filesystem::path&, const processors::LlamaSamplerParams&,
const processors::LlamaContextParams&) {
+ [&](const std::filesystem::path&, const
std::optional<std::filesystem::path>&, const processors::LlamaSamplerParams&,
const processors::LlamaContextParams&) {
return std::make_unique<MockLlamaContext>();
}));
LogTestController::getInstance().setTrace<processors::RunLlamaCppInference>();
@@ -236,7 +247,7 @@ TEST_CASE("Top K property empty and invalid values are
handled properly") {
std::optional<int32_t> test_top_k = 0;
minifi::test::SingleProcessorTestController
controller(minifi::test::utils::make_custom_c_processor<processors::RunLlamaCppInference>(
core::ProcessorMetadata{utils::Identifier{}, "RunLlamaCppInference",
logging::LoggerFactory<processors::RunLlamaCppInference>::getLogger()},
- [&](const std::filesystem::path&, const processors::LlamaSamplerParams&
sampler_params, const processors::LlamaContextParams&) {
+ [&](const std::filesystem::path&, const
std::optional<std::filesystem::path>&, const processors::LlamaSamplerParams&
sampler_params, const processors::LlamaContextParams&) {
test_top_k = sampler_params.top_k;
return std::make_unique<MockLlamaContext>();
}));
@@ -269,7 +280,7 @@ TEST_CASE("Error handling during generation and applying
template") {
minifi::test::SingleProcessorTestController
controller(minifi::test::utils::make_custom_c_processor<processors::RunLlamaCppInference>(
core::ProcessorMetadata{utils::Identifier{}, "RunLlamaCppInference",
logging::LoggerFactory<processors::RunLlamaCppInference>::getLogger()},
- [&](const std::filesystem::path&, const processors::LlamaSamplerParams&,
const processors::LlamaContextParams&) {
+ [&](const std::filesystem::path&, const
std::optional<std::filesystem::path>&, const processors::LlamaSamplerParams&,
const processors::LlamaContextParams&) {
return std::move(mock_llama_context);
}));
LogTestController::getInstance().setTrace<processors::RunLlamaCppInference>();
@@ -287,7 +298,7 @@ TEST_CASE("Error handling during generation and applying
template") {
TEST_CASE("Route flow file to failure when prompt and input data is empty") {
minifi::test::SingleProcessorTestController
controller(minifi::test::utils::make_custom_c_processor<processors::RunLlamaCppInference>(
core::ProcessorMetadata{utils::Identifier{}, "RunLlamaCppInference",
logging::LoggerFactory<processors::RunLlamaCppInference>::getLogger()},
- [&](const std::filesystem::path&, const processors::LlamaSamplerParams&,
const processors::LlamaContextParams&) {
+ [&](const std::filesystem::path&, const
std::optional<std::filesystem::path>&, const processors::LlamaSamplerParams&,
const processors::LlamaContextParams&) {
return std::make_unique<MockLlamaContext>();
}));
LogTestController::getInstance().setTrace<processors::RunLlamaCppInference>();
@@ -307,7 +318,7 @@ TEST_CASE("System prompt is optional") {
auto mock_llama_context_ptr = mock_llama_context.get();
minifi::test::SingleProcessorTestController
controller(minifi::test::utils::make_custom_c_processor<processors::RunLlamaCppInference>(
core::ProcessorMetadata{utils::Identifier{}, "RunLlamaCppInference",
logging::LoggerFactory<processors::RunLlamaCppInference>::getLogger()},
- [&](const std::filesystem::path&, const processors::LlamaSamplerParams&,
const processors::LlamaContextParams&) {
+ [&](const std::filesystem::path&, const
std::optional<std::filesystem::path>&, const processors::LlamaSamplerParams&,
const processors::LlamaContextParams&) {
return std::move(mock_llama_context);
}));
LogTestController::getInstance().setTrace<processors::RunLlamaCppInference>();
@@ -329,7 +340,7 @@ TEST_CASE("System prompt is optional") {
TEST_CASE("Test output metrics") {
auto processor =
minifi::test::utils::make_custom_c_processor<processors::RunLlamaCppInference>(
core::ProcessorMetadata{utils::Identifier{}, "RunLlamaCppInference",
logging::LoggerFactory<processors::RunLlamaCppInference>::getLogger()},
- [&](const std::filesystem::path&, const processors::LlamaSamplerParams&,
const processors::LlamaContextParams&) {
+ [&](const std::filesystem::path&, const
std::optional<std::filesystem::path>&, const processors::LlamaSamplerParams&,
const processors::LlamaContextParams&) {
return std::make_unique<MockLlamaContext>();
});
auto processor_metrics = processor->getMetrics();
@@ -357,4 +368,63 @@ TEST_CASE("Test output metrics") {
CHECK(c2_metrics[0].children[c2_metrics[0].children.size() -
1].value.to_string() == "6");
}
+TEST_CASE("Prompt is generated correctly for multimodal inference") {
+ auto mock_llama_context = std::make_unique<MockLlamaContext>();
+ auto mock_llama_context_ptr = mock_llama_context.get();
+ std::filesystem::path test_model_path;
+ std::optional<std::filesystem::path> test_model_path_option;
+ minifi::test::SingleProcessorTestController
controller(minifi::test::utils::make_custom_c_processor<processors::RunLlamaCppInference>(
+ core::ProcessorMetadata{utils::Identifier{}, "RunLlamaCppInference",
logging::LoggerFactory<processors::RunLlamaCppInference>::getLogger()},
+ [&](const std::filesystem::path& model_path, const
std::optional<std::filesystem::path>& multimodal_model_path, const
processors::LlamaSamplerParams&, const processors::LlamaContextParams&) {
+ test_model_path = model_path;
+ test_model_path_option = multimodal_model_path;
+ if (multimodal_model_path) {
+ mock_llama_context->setMultimodal();
+ }
+ return std::move(mock_llama_context);
+ }));
+
LogTestController::getInstance().setTrace<processors::RunLlamaCppInference>();
+
REQUIRE(controller.getProcessor()->setProperty(processors::RunLlamaCppInference::ModelPath.name,
"/path/to/model"));
+
REQUIRE(controller.getProcessor()->setProperty(processors::RunLlamaCppInference::MultiModalModelPath.name,
"/path/to/mm-model"));
+
REQUIRE(controller.getProcessor()->setProperty(processors::RunLlamaCppInference::Prompt.name,
"What is on the image?"));
+
+
+ SECTION("Flowfile contains data") {
+ auto results = controller.trigger(minifi::test::InputFlowFileData{.content
= "<some image data>", .attributes = {}});
+ CHECK(test_model_path == "/path/to/model");
+ CHECK(test_model_path_option == "/path/to/mm-model");
+ REQUIRE(results.at(processors::RunLlamaCppInference::Success).size() == 1);
+ auto& output_flow_file =
results.at(processors::RunLlamaCppInference::Success)[0];
+ CHECK(controller.plan->getContent(output_flow_file) == "Test generated
content");
+ CHECK(mock_llama_context_ptr->getInput() == "Test input");
+ REQUIRE(mock_llama_context_ptr->getMessages().size() == 2);
+ CHECK(mock_llama_context_ptr->getMessages()[1].role == "user");
+ CHECK(mock_llama_context_ptr->getMessages()[1].content == "Input data (or
flow file content):\n<__media__>\n\nWhat is on the image?");
+ }
+
+ SECTION("Flowfile is empty") {
+ auto results = controller.trigger(minifi::test::InputFlowFileData{.content
= "", .attributes = {}});
+ REQUIRE(results.at(processors::RunLlamaCppInference::Failure).size() == 1);
+ }
+}
+
+TEST_CASE("Can write content to attribute") {
+ minifi::test::SingleProcessorTestController
controller(minifi::test::utils::make_custom_c_processor<processors::RunLlamaCppInference>(
+ core::ProcessorMetadata{utils::Identifier{}, "RunLlamaCppInference",
logging::LoggerFactory<processors::RunLlamaCppInference>::getLogger()},
+ [&](const std::filesystem::path&, const
std::optional<std::filesystem::path>&, const processors::LlamaSamplerParams&,
const processors::LlamaContextParams&) {
+ return std::make_unique<MockLlamaContext>();
+ }));
+
LogTestController::getInstance().setTrace<processors::RunLlamaCppInference>();
+
REQUIRE(controller.getProcessor()->setProperty(processors::RunLlamaCppInference::ModelPath.name,
"/path/to/model"));
+
REQUIRE(controller.getProcessor()->setProperty(processors::RunLlamaCppInference::Prompt.name,
"What is love?"));
+
REQUIRE(controller.getProcessor()->setProperty(processors::RunLlamaCppInference::OutputAttributeName.name,
"DontHurtMe"));
+
+
+ auto results = controller.trigger(minifi::test::InputFlowFileData{.content =
"Some content", .attributes = {}});
+ REQUIRE(results.at(processors::RunLlamaCppInference::Success).size() == 1);
+ auto& output_flow_file =
results.at(processors::RunLlamaCppInference::Success)[0];
+ CHECK(controller.plan->getContent(output_flow_file) == "Some content");
+ CHECK(output_flow_file->getAttribute("DontHurtMe") == "Test generated
content");
+}
+
} // namespace org::apache::nifi::minifi::extensions::llamacpp::test
diff --git a/extensions/llamacpp/tests/features/environment.py
b/extensions/llamacpp/tests/features/environment.py
index 9f23b7656..bcef201bf 100644
--- a/extensions/llamacpp/tests/features/environment.py
+++ b/extensions/llamacpp/tests/features/environment.py
@@ -14,6 +14,7 @@
# limitations under the License.
from textwrap import dedent
+from pathlib import Path
from minifi_behave.containers.docker_image_builder import DockerImageBuilder
from minifi_behave.core.hooks import common_before_scenario
from minifi_behave.core.hooks import common_after_scenario
@@ -29,7 +30,9 @@ def before_all(context: MinifiTestContext):
dockerfile = dedent("""\
FROM {base_image}
- RUN mkdir {models_path} && wget
https://huggingface.co/bartowski/Qwen2-0.5B-Instruct-GGUF/resolve/main/Qwen2-0.5B-Instruct-IQ3_M.gguf
--directory-prefix={models_path}
+ RUN mkdir {models_path}
+ RUN wget
https://huggingface.co/bartowski/Qwen2-VL-2B-Instruct-GGUF/resolve/main/Qwen2-VL-2B-Instruct-Q3_K_M.gguf
--directory-prefix={models_path}
+ RUN wget
https://huggingface.co/bartowski/Qwen2-VL-2B-Instruct-GGUF/resolve/main/mmproj-Qwen2-VL-2B-Instruct-f16.gguf
--directory-prefix={models_path}
""".format(base_image=minifi_container_image,
models_path='/tmp/models'))
builder = DockerImageBuilder(
@@ -42,6 +45,7 @@ def before_all(context: MinifiTestContext):
def before_scenario(context: MinifiTestContext, scenario):
context.minifi_container_image = "apacheminificpp:llama"
common_before_scenario(context, scenario)
+ context.resource_dir = Path(__file__).resolve().parent / "resources"
def after_scenario(context, scenario):
diff --git a/extensions/llamacpp/tests/features/llamacpp.feature
b/extensions/llamacpp/tests/features/llamacpp.feature
index 437461c08..5ff088db3 100644
--- a/extensions/llamacpp/tests/features/llamacpp.feature
+++ b/extensions/llamacpp/tests/features/llamacpp.feature
@@ -18,11 +18,27 @@ Feature: Run language model inference using LlamaCpp
processor
Scenario: Test inference with a small model
Given a GenerateFlowFile processor with the "File Size" property set to
"0B"
- And a RunLlamaCppInference processor with the "Model Path" property set to
"/tmp/models/Qwen2-0.5B-Instruct-IQ3_M.gguf"
+ And a RunLlamaCppInference processor with the "Model Path" property set to
"/tmp/models/Qwen2-VL-2B-Instruct-Q3_K_M.gguf"
And the "Prompt" property of the RunLlamaCppInference processor is set to
"Repeat after me: banana banana banana"
+ And the "Temperature" property of the RunLlamaCppInference processor is
set to "0"
And a LogAttribute processor with the "Log Payload" property set to "true"
And the "success" relationship of the GenerateFlowFile processor is
connected to the RunLlamaCppInference
And the "success" relationship of the RunLlamaCppInference processor is
connected to the LogAttribute
When all instances start up
Then the Minifi logs contain the following message: "banana" in less than
60 seconds
+
+ Scenario: Test multimodal inference with a small model
+ Given a GetFile processor with the "Input Directory" property set to
"/tmp/input"
+ And a directory at "/tmp/input" has a file with the content from
"test-image.png"
+ And a RunLlamaCppInference processor with the "Model Path" property set to
"/tmp/models/Qwen2-VL-2B-Instruct-Q3_K_M.gguf"
+ And the "Prompt" property of the RunLlamaCppInference processor is set to
"Output only what is written on the image."
+ And the "MultiModal Model Path" property of the RunLlamaCppInference
processor is set to "/tmp/models/mmproj-Qwen2-VL-2B-Instruct-f16.gguf"
+ And the "Temperature" property of the RunLlamaCppInference processor is
set to "0"
+ And a PutFile processor with the "Directory" property set to "/tmp/output"
+ And the "success" relationship of the GetFile processor is connected to
the RunLlamaCppInference
+ And the "success" relationship of the RunLlamaCppInference processor is
connected to the PutFile
+
+ When all instances start up
+ Then a single file with the content "minifi" is placed in the
"/tmp/output" directory in less than 600 seconds
+
diff --git a/extensions/llamacpp/tests/features/resources/test-image.png
b/extensions/llamacpp/tests/features/resources/test-image.png
new file mode 100644
index 000000000..f6d7720d5
Binary files /dev/null and
b/extensions/llamacpp/tests/features/resources/test-image.png differ
diff --git a/thirdparty/llamacpp/cpp-23-fixes.patch
b/thirdparty/llamacpp/cpp-23-fixes.patch
deleted file mode 100644
index 0e84e4395..000000000
--- a/thirdparty/llamacpp/cpp-23-fixes.patch
+++ /dev/null
@@ -1,24 +0,0 @@
-From 072bd8ce7e10a0fffb1e2bc755c2964e472909ed Mon Sep 17 00:00:00 2001
-From: Martin Zink <[email protected]>
-Date: Tue, 22 Jul 2025 12:49:42 +0200
-Subject: [PATCH] c++23 fixes
-
----
- src/llama-hparams.cpp | 2 ++
- 1 file changed, 2 insertions(+)
-
-diff --git a/src/llama-hparams.cpp b/src/llama-hparams.cpp
-index c6c67d26..db36de4d 100644
---- a/src/llama-hparams.cpp
-+++ b/src/llama-hparams.cpp
-@@ -1,5 +1,7 @@
- #include "llama-hparams.h"
-
-+#include <algorithm>
-+
- #include "ggml.h"
-
- void llama_hparams::set_swa_pattern(uint32_t n_pattern) {
---
-2.39.5 (Apple Git-154)
-
diff --git a/thirdparty/llamacpp/lu8_macro_fix.patch
b/thirdparty/llamacpp/lu8_macro_fix.patch
deleted file mode 100644
index a1b92d28b..000000000
--- a/thirdparty/llamacpp/lu8_macro_fix.patch
+++ /dev/null
@@ -1,17 +0,0 @@
-diff --git a/src/llama-chat.cpp b/src/llama-chat.cpp
-index dd27a381..47550954 100644
---- a/src/llama-chat.cpp
-+++ b/src/llama-chat.cpp
-@@ -6,11 +6,7 @@
- #include <sstream>
- #include <algorithm>
-
--#if __cplusplus >= 202000L
-- #define LU8(x) (const char*)(u8##x)
--#else
-- #define LU8(x) u8##x
--#endif
-+#define LU8(x) reinterpret_cast<const char*>(u8##x)
-
- // trim whitespace from the beginning and end of a string
- static std::string trim(const std::string & str) {
diff --git a/thirdparty/llamacpp/mtmd-fix.patch
b/thirdparty/llamacpp/mtmd-fix.patch
new file mode 100644
index 000000000..6ac631d4e
--- /dev/null
+++ b/thirdparty/llamacpp/mtmd-fix.patch
@@ -0,0 +1,44 @@
+diff --color=auto -rupN llama.cpp-b8944/CMakeLists.txt
llama.cpp-b8944-patched/CMakeLists.txt
+--- llama.cpp-b8944/CMakeLists.txt 2026-04-27 08:30:55
++++ llama.cpp-b8944-patched/CMakeLists.txt 2026-04-27 13:49:25
+@@ -191,6 +191,7 @@ add_subdirectory(src)
+ #
+
+ add_subdirectory(src)
++add_subdirectory(tools/mtmd)
+
+ #
+ # utils, programs, examples and tests
+diff --color=auto -rupN llama.cpp-b8944/common/ngram-mod.cpp
llama.cpp-b8944-patched/common/ngram-mod.cpp
+--- llama.cpp-b8944/common/ngram-mod.cpp 2026-04-27 08:30:55
++++ llama.cpp-b8944-patched/common/ngram-mod.cpp 2026-04-30 08:28:08
+@@ -1,4 +1,5 @@
+ #include "ngram-mod.h"
++#include <algorithm>
+
+ //
+ // common_ngram_mod
+diff --color=auto -rupN llama.cpp-b8944/tools/mtmd/CMakeLists.txt
llama.cpp-b8944-patched/tools/mtmd/CMakeLists.txt
+--- llama.cpp-b8944/tools/mtmd/CMakeLists.txt 2026-04-27 08:30:55
++++ llama.cpp-b8944-patched/tools/mtmd/CMakeLists.txt 2026-04-27 13:50:45
+@@ -101,20 +101,6 @@ endif()
+ endif()
+ endif()
+
+-add_executable(llama-llava-cli deprecation-warning.cpp)
+-add_executable(llama-gemma3-cli deprecation-warning.cpp)
+-add_executable(llama-minicpmv-cli deprecation-warning.cpp)
+-add_executable(llama-qwen2vl-cli deprecation-warning.cpp)
+-
+-set(TARGET llama-mtmd-cli)
+-add_executable (${TARGET} mtmd-cli.cpp)
+-set_target_properties (${TARGET} PROPERTIES OUTPUT_NAME llama-mtmd-cli)
+-if(LLAMA_TOOLS_INSTALL)
+- install(TARGETS ${TARGET} RUNTIME)
+-endif()
+-target_link_libraries (${TARGET} PRIVATE llama-common mtmd Threads::Threads)
+-target_compile_features(${TARGET} PRIVATE cxx_std_17)
+-
+ # mtmd-debug tool
+ add_executable(llama-mtmd-debug debug/mtmd-debug.cpp)
+ set_target_properties(llama-mtmd-debug PROPERTIES OUTPUT_NAME
llama-mtmd-debug)