// Local NDJSON helper around the pinned llama.cpp encoder. stdout is exclusively // protocol output; llama diagnostics go to stderr. No model download or HTTP. #include "llama.h" #include "ggml-backend.h" #include "mtmd.h" #include "nlohmann/json.hpp" #include #include #include #include #include #include #include #include #include #include #include #ifdef SEARCHI_CPU_DISPATCH #define WIN32_LEAN_AND_MEAN #define NOMINMAX #include #include #endif using json = nlohmann::json; constexpr size_t max_request_bytes = 16 * 1024 * 1024; constexpr size_t max_passage_bytes = 1024 * 1024; constexpr size_t max_text_bytes = 65536; constexpr size_t max_windows = 256; constexpr int context_tokens = 2048; constexpr int dimensions = 768; static void require(bool condition, const char * message) { if (!condition) throw std::runtime_error(message); } struct InferenceError : std::runtime_error { using std::runtime_error::runtime_error; }; static void require_inference(bool condition, const char * message) { if (!condition) throw InferenceError(message); } class Encoder { llama_model * model = nullptr; llama_context * context = nullptr; llama_batch_ext * batch = nullptr; const llama_vocab * vocab = nullptr; mtmd_context * media = nullptr; std::string model_path; void release() { if (media) mtmd_free(media); if (batch) llama_batch_ext_free(batch); if (context) llama_free(context); if (model) llama_model_free(model); batch = nullptr; context = nullptr; model = nullptr; vocab = nullptr; media = nullptr; } bool open(bool gpu) { auto mp = llama_model_default_params(); static ggml_backend_dev_t cpu_devices[] = {nullptr}; if (!gpu) mp.devices = cpu_devices; mp.n_gpu_layers = gpu ? 999 : 0; mp.progress_callback = [](float,void *) {return true;}; model = llama_model_load_from_file(model_path.c_str(),mp); if (!model) return false; auto cp = llama_context_default_params(); cp.n_ctx = context_tokens; cp.n_batch = context_tokens; cp.n_ubatch = context_tokens; cp.n_seq_max = 1; cp.kv_unified = true; cp.embeddings = true; cp.offload_kqv = gpu; cp.op_offload = gpu; cp.pooling_type = LLAMA_POOLING_TYPE_MEAN; cp.n_threads = threads; cp.n_threads_batch = threads; context = llama_init_from_model(model,cp); if (!context) {release(); return false;} require(llama_model_n_embd_out(model) == dimensions,"EmbeddingGemma 2 output must have 768 dimensions"); vocab = llama_model_get_vocab(model); batch = llama_batch_ext_init(context); if (!batch) {release(); return false;} device = gpu ? "gpu" : "cpu"; return true; } public: std::string device; std::string cpu_backend_module; bool vulkan_module_loaded = false; int threads; Encoder(const std::string & path, int cpu_threads, bool want_gpu) : model_path(path),threads(cpu_threads) { llama_log_set([](ggml_log_level level, const char * text, void *) { if (level >= GGML_LOG_LEVEL_WARN) std::fputs(text,stderr); },nullptr); mtmd_log_set([](ggml_log_level level, const char * text, void *) { if (level >= GGML_LOG_LEVEL_WARN) std::fputs(text,stderr); },nullptr); #ifdef SEARCHI_CPU_DISPATCH // Only load our adjacent modules: no current-directory or environment // plugin discovery. CPU-only startup never loads the Vulkan module. std::vector executable(32768); DWORD length = GetModuleFileNameW(nullptr, executable.data(), DWORD(executable.size())); require(length > 0 && length < executable.size(), "Cannot locate packaged worker backends"); const auto directory = std::filesystem::path(std::wstring(executable.data(), length)).parent_path().u8string(); if (want_gpu) { ggml_backend_load_all_from_path(directory.c_str()); } else { ggml_backend_load_cpu_only_from_path(directory.c_str()); } require(ggml_backend_reg_by_name("CPU") != nullptr, "Cannot load compatible packaged CPU backend"); const auto cpu_reg = ggml_backend_reg_by_name("CPU"); const auto feature_proc = ggml_backend_reg_get_proc_address(cpu_reg, "ggml_backend_get_features"); HMODULE cpu_module = nullptr; if (feature_proc && GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(feature_proc), &cpu_module)) { DWORD module_length = GetModuleFileNameW(cpu_module, executable.data(), DWORD(executable.size())); if (module_length > 0 && module_length < executable.size()) { cpu_backend_module = std::filesystem::path(std::wstring(executable.data(), module_length)).filename().u8string(); } } vulkan_module_loaded = GetModuleHandleW(L"ggml-vulkan.dll") != nullptr; #endif llama_backend_init(); bool use_gpu = want_gpu && llama_supports_gpu_offload(); require(open(use_gpu) || (use_gpu && open(false)),"Cannot initialize local EmbeddingGemma 2 model/context"); } ~Encoder() { release(); llama_backend_free(); } bool healthy() const {return context != nullptr && batch != nullptr;} void ensure_media(const std::string & projector) { if (media) return; auto params = mtmd_context_params_default(); params.use_gpu = device == "gpu"; params.n_threads = threads; params.print_timings = false; params.warmup = false; params.image_min_tokens = 1; params.image_max_tokens = 1120; media = mtmd_init_from_file(projector.c_str(),model,params); require_inference(media != nullptr,"Cannot initialize Google media projector"); } std::vector embed_bitmap(mtmd_bitmap * bitmap) { std::unique_ptr chunks(mtmd_input_chunks_init(),mtmd_input_chunks_free); require_inference(chunks != nullptr,"Cannot allocate media chunks"); const char * marker = mtmd_default_marker(); mtmd_input_text text{marker,std::strlen(marker),true,true}; const mtmd_bitmap * bitmaps[] = {bitmap}; require_inference(mtmd_tokenize(media,chunks.get(),&text,bitmaps,1) == 0,"Media tokenization failed"); llama_batch_ext_clear(batch); size_t position = 0; auto place = [&](int index) { require_inference(index >= 0 && position < context_tokens,"Media batch exceeds context"); llama_pos pos[4] = {llama_pos(position++),0,0,0}; require_inference(llama_batch_ext_set_pos(batch,index,pos) && llama_batch_ext_set_output_embd(batch,index,true),"Cannot place media row"); }; for (size_t i=0;i0 && std::isfinite(norm),"Invalid media vector norm"); std::vector vector(dimensions);for(int i=0;i tokenize(const std::string & text, bool specials) const { std::vector tokens(text.size() + 8); int count = llama_tokenize(vocab, text.data(), int(text.size()), tokens.data(), int(tokens.size()), specials, false); if (count < 0) { tokens.resize(size_t(-count)); count = llama_tokenize(vocab, text.data(), int(text.size()), tokens.data(), int(tokens.size()), specials, false); } require(count >= 0, "Tokenizer failed"); tokens.resize(size_t(count)); return tokens; } std::string decode(const std::vector & tokens, size_t count) const { std::vector text(max_passage_bytes + 64); int length = llama_detokenize(vocab, tokens.data(), int(count), text.data(), int(text.size()), false, false); require(length >= 0, "Detokenized passage exceeds byte budget"); return std::string(text.data(),size_t(length)); } static std::string document_prefix(const std::string & title) { require(title.size() <= 512, "Document title exceeds 512 UTF-8 bytes"); return "title: " + (title.empty() ? "none" : title) + " | text: "; } std::vector embed(const std::string & text, int cap, json * timings = nullptr) { using clock = std::chrono::steady_clock; auto started = timings ? clock::now() : clock::time_point{}; auto mark = [&](const char * name) { if (timings) { auto now = clock::now(); (*timings)[name] = std::chrono::duration_cast(now-started).count(); started = now; } }; require(!text.empty() && text.size() <= max_text_bytes, "Embedding text must be nonempty and at most 64 KiB"); auto tokens = tokenize(text,true); require(!tokens.empty() && tokens.size() <= size_t(cap), "Prompt exceeds model token budget; split document windows first"); if (timings) (*timings)["token_count"] = tokens.size(); mark("tokenization_us"); llama_batch_ext_clear(batch); for (size_t i = 0; i < tokens.size(); ++i) { int index = llama_batch_ext_add_token(batch,0,tokens[i]); require(index >= 0, "Embedding batch is full"); llama_pos position[4] = {llama_pos(i),0,0,0}; require(llama_batch_ext_set_pos(batch,index,position), "Cannot set encoder position"); require(llama_batch_ext_set_output_embd(batch,index,true), "Cannot select embedding output"); } mark("batch_prepare_us"); require_inference(llama_process(context,LLAMA_PROCESS_TYPE_ENCODE,batch) == 0, "llama_process encoder failed"); mark("inference_us"); const float * pooled = llama_get_embeddings_seq(context,0); require_inference(pooled != nullptr, "Encoder returned no pooled vector"); double norm = 0; for (int i = 0; i < dimensions; ++i) { require_inference(std::isfinite(pooled[i]), "Nonfinite encoder output"); norm += double(pooled[i])*pooled[i]; } require_inference(norm > 0 && std::isfinite(norm), "Zero or nonfinite embedding norm"); std::vector vector(dimensions); for (int i = 0; i < dimensions; ++i) vector[i] = float(pooled[i]/std::sqrt(norm)); mark("pool_normalize_us"); return vector; } json windows(const std::string & text, const std::string & title) const { require(!text.empty() && text.size() <= max_passage_bytes, "Passage exceeds 1 MiB or is empty"); auto tokens = tokenize(text,false); require(!tokens.empty(), "Passage has no tokens"); size_t prompt = tokenize(document_prefix(title),true).size(); require(prompt + 132 < size_t(context_tokens), "Document prefix exceeds token budget"); // SentencePiece may normalize source characters. In that case decoded // token lengths are not source offsets: split the original UTF-8 bytes // and verify each complete prefixed fragment against the real tokenizer. if (decode(tokens,tokens.size()) != text) { const auto prefix = document_prefix(title); json result = json::array(); if (prefix.size()+text.size() <= max_text_bytes && tokenize(prefix+text,true).size() <= size_t(context_tokens)) { return json::array({{{"text",text},{"start_byte",0},{"end_byte",text.size()}}}); } std::vector boundaries; for (size_t i=0;i start, "Cannot fit source fragment in model context"); auto fragment = text.substr(start,best-start); require(tokenize(prefix+fragment,true).size() <= size_t(context_tokens), "Source fragment exceeds context budget"); result.push_back({{"text",fragment},{"start_byte",start},{"end_byte",best}}); if (best == text.size()) break; // Bounded original-byte overlap; always advance, even for // unusual Unicode/token expansion. No decoded text is indexed. size_t next = best - std::min(size_t(128),(best-start)/2); start_index = size_t(std::lower_bound(boundaries.begin(),boundaries.end(),next)-boundaries.begin()); } return result; } size_t budget = size_t(context_tokens) - prompt - 4; size_t stride = budget - 128; json result = json::array(); for (size_t start = 0; start < tokens.size(); start += stride) { require(result.size() < max_windows, "Passage exceeds 256 windows"); size_t end = std::min(start + budget,tokens.size()); size_t begin_byte = start ? decode(tokens,start).size() : 0; size_t end_byte = decode(tokens,end).size(); // Token pieces can split a UTF-8 character. Enlarge to valid source // boundaries; the four-token reserve covers boundary re-tokenization. while (begin_byte > 0 && begin_byte < text.size() && (uint8_t(text[begin_byte]) & 0xc0) == 0x80) --begin_byte; while (end_byte < text.size() && (uint8_t(text[end_byte]) & 0xc0) == 0x80) ++end_byte; require(begin_byte < end_byte && end_byte <= text.size(), "Invalid source window byte range"); auto fragment = text.substr(begin_byte,end_byte-begin_byte); require(fragment.size() + document_prefix(title).size() <= max_text_bytes, "Window exceeds text byte budget"); require(tokenize(document_prefix(title)+fragment,true).size() <= size_t(context_tokens), "Window re-tokenization exceeds context budget"); result.push_back({{"text",fragment},{"start_byte",begin_byte},{"end_byte",end_byte}}); if (end == tokens.size()) break; } return result; } json handle(const json & request) { auto op = request.at("op").get(); if(op=="image" || op=="audio") { auto encoded=request.at("data").get(); require(encoded.size()<=12000000 && encoded.size()%4==0,"Invalid media payload length"); static const std::string alphabet="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; std::vector bytes; bytes.reserve(encoded.size()/4*3); unsigned accumulator=0;int bits=0;bool padding=false; for(char c:encoded){if(c=='='){padding=true;continue;} require(!padding,"Invalid base64 padding");auto n=alphabet.find(c);require(n!=std::string::npos,"Invalid base64");accumulator=(accumulator<<6)|unsigned(n);bits+=6;if(bits>=8){bits-=8;bytes.push_back(static_cast(accumulator>>bits));}} auto projector=request.at("projector").get(); require(!projector.empty() && projector.size()<32768,"Invalid projector path"); ensure_media(projector); mtmd_bitmap * raw=nullptr; if(op=="image") { int w=request.at("width"),h=request.at("height"); require(w>=48 && h>=48 && w%48==0 && h%48==0 && uint64_t(w)*h<=1120*48*48,"Invalid bounded image dimensions"); require(bytes.size()==size_t(w)*h*3,"RGB image length mismatch"); raw=mtmd_bitmap_init(w,h,bytes.data()); } else { require(bytes.size()%4==0 && !bytes.empty() && bytes.size()/4<=30*16000,"Audio must be at most30seconds mono16kHz PCM"); std::vector pcm(bytes.size()/4); std::memcpy(pcm.data(),bytes.data(),bytes.size()); for(float f:pcm)require(std::isfinite(f) && std::abs(f)<=1,"Audio PCM outside normalized range"); raw=mtmd_bitmap_init_from_audio(pcm.size(),pcm.data()); } std::unique_ptr bitmap(raw,mtmd_bitmap_free); require_inference(bitmap != nullptr,"Cannot allocate media bitmap"); return {{"vectors",json::array({embed_bitmap(bitmap.get())})}}; } if (op == "windows") return {{"windows",windows(request.at("text").get(),request.value("title",std::string()))}}; if (op == "query" || op == "code_query") { bool debug = request.value("debug_performance",false); json timings; auto prefix = op == "code_query" ? "task: code retrieval | query: " : "task: search result | query: "; auto vector = embed(prefix+request.at("text").get(),512,debug ? &timings : nullptr); json response = {{"vectors",json::array({vector})}}; if (debug) response["performance"] = timings; return response; } if (op == "documents") { const auto & texts = request.at("texts"); require(texts.is_array() && !texts.empty() && texts.size() <= 8,"Batch size must be between 1 and 8"); auto prefix = document_prefix(request.value("title",std::string())); json vectors = json::array(); for (const auto & text : texts) vectors.push_back(embed(prefix+text.get(),context_tokens)); return {{"vectors",vectors}}; } throw std::runtime_error("Unknown embedding operation"); } }; int main(int argc, char ** argv) { try { std::string path, device = "auto"; int threads = 1; for (int i = 1; i < argc; ++i) { require(i+1 < argc,"Worker options require values"); std::string option = argv[i++], value = argv[i]; if (option == "--model") path = value; else if (option == "--threads") threads = std::stoi(value); else if (option == "--device") device = value; else throw std::runtime_error("Unknown worker option"); } require(!path.empty() && threads >= 1 && threads <= 8,"Model path required; threads must be 1–8"); require(device == "cpu" || device == "auto","Device must be cpu or auto"); bool force_cpu = std::getenv("SEARCHI_FORCE_CPU") && std::string(std::getenv("SEARCHI_FORCE_CPU")) == "1"; // Disable backend registration before llama initializes its registry. // A GPU-capable distribution can therefore be tested on CPU without // probing/enabling the Vulkan provider in the Windows VM. if (force_cpu || device == "cpu") { #ifdef _WIN32 require(_putenv_s("GGML_CPU_ONLY", "1") == 0, "Cannot restrict CPU startup"); require(_putenv_s("GGML_DISABLE_VULKAN", "1") == 0, "Cannot disable Vulkan for CPU startup"); #else require(setenv("GGML_CPU_ONLY", "1", 1) == 0, "Cannot restrict CPU startup"); require(setenv("GGML_DISABLE_VULKAN", "1", 1) == 0, "Cannot disable Vulkan for CPU startup"); #endif } Encoder encoder(path,threads,device == "auto" && !force_cpu); std::cout << json({{"ready",true},{"model","google/embeddinggemma-2"},{"dimensions",dimensions}, {"device",encoder.device},{"threads",threads},{"runtime",SEARCHI_LLAMA_TAG},{"media_supported",true},{"code_query_supported",true}, {"cpu_backend_module",encoder.cpu_backend_module},{"vulkan_module_loaded",encoder.vulkan_module_loaded}}).dump() << '\n' << std::flush; for (;;) { std::string line; char c; while (std::cin.get(c) && c != '\n') { require(line.size() < max_request_bytes,"Request exceeds 16 MiB"); line.push_back(c); } if (!std::cin && line.empty()) break; json response; try { auto request = json::parse(line); try {response = encoder.handle(request);} catch (const InferenceError &) { if (encoder.device != "gpu") throw; encoder.retry_cpu(); response = encoder.handle(request); // Exactly one CPU retry. response["fallback"] = "gpu_inference_failure"; } response["ok"] = true; } catch (const std::exception & error) { response = {{"ok",false},{"error",error.what()},{"fatal",!encoder.healthy()}}; } response["device"] = encoder.device; auto encoded = response.dump(); require(encoded.size() <= 4*1024*1024,"Response exceeds 4 MiB"); std::cout << encoded << '\n' << std::flush; if (!encoder.healthy()) return 1; } return 0; } catch (const std::exception & error) { std::cerr << "Searchi Gemma worker: " << error.what() << '\n'; return 1; } }