From d9f918d2d06079b4336688e819eee821c8a9cd9e Mon Sep 17 00:00:00 2001 From: Xuan-Son Nguyen Date: Sat, 22 Aug 2026 16:28:28 +0200 Subject: [PATCH 01/28] common: add json.h abstraction (#27511) * add common/json * migrate common * adapt jinja * migrate server * big wip * migrate tests * wip * revert some excessive changes * wip * wip 2 * revert redundant changes * fix server crash * various fixes * fix ci * harden a bit * clean up * rm json-shim * add some comments * rm redundant decl --- common/CMakeLists.txt | 2 + common/arg.cpp | 7 +- common/chat-auto-parser-generator.cpp | 5 +- common/chat-auto-parser-helpers.cpp | 3 - common/chat-auto-parser.h | 4 +- common/chat-diff-analyzer.cpp | 6 +- common/chat-peg-parser.cpp | 4 +- common/chat-peg-parser.h | 12 +- common/chat.cpp | 38 +- common/chat.h | 19 +- common/download.cpp | 16 +- common/hf-cache.cpp | 18 +- common/jinja/README.md | 2 +- common/jinja/caps.cpp | 4 +- common/jinja/value.cpp | 6 +- common/jinja/value.h | 2 +- common/json-schema-to-grammar.cpp | 21 +- common/json-schema-to-grammar.h | 12 +- common/json.cpp | 437 +++++++++++++++++++ common/json.h | 354 +++++++++++++++ common/peg-parser.cpp | 31 +- common/peg-parser.h | 10 +- tests/peg-parser/test-json-serialization.cpp | 4 +- tests/peg-parser/tests.h | 8 +- tests/test-chat-peg-parser.cpp | 34 +- tests/test-chat-template.cpp | 8 +- tests/test-chat.cpp | 4 +- tests/test-grammar-integration.cpp | 4 +- tests/test-jinja.cpp | 8 +- tests/test-json-schema-to-grammar.cpp | 8 +- tests/test-model-resolution.cpp | 6 +- tools/cli/cli-context.cpp | 11 +- tools/parser/debug-template-parser.cpp | 4 +- tools/parser/template-analysis.cpp | 4 +- tools/server/server-chat.cpp | 2 +- tools/server/server-chat.h | 4 +- tools/server/server-common.cpp | 2 +- tools/server/server-common.h | 11 +- tools/server/server-context.cpp | 29 +- tools/server/server-context.h | 2 +- tools/server/server-models.cpp | 2 +- tools/server/server-schema.cpp | 5 +- tools/server/server-task.cpp | 28 +- tools/server/server-task.h | 1 - tools/server/server-tools.cpp | 2 +- 45 files changed, 987 insertions(+), 217 deletions(-) create mode 100644 common/json.cpp create mode 100644 common/json.h diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 54691da3f..36f1e0cd5 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -81,6 +81,8 @@ add_library(${TARGET} imatrix-loader.cpp imatrix-loader.h json-schema-to-grammar.cpp + json.cpp + json.h llguidance.cpp log.cpp log.h diff --git a/common/arg.cpp b/common/arg.cpp index 3da1d61f4..86f8610a5 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -5,6 +5,7 @@ #include "common.h" #include "download.h" #include "json-schema-to-grammar.h" +#include "json.h" #include "llama.h" #include "log.h" #include "sampling.h" @@ -21,9 +22,6 @@ #include #endif -#define JSON_ASSERT GGML_ASSERT -#include - #include #include #include @@ -32,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -55,7 +54,7 @@ #define LLAMA_MAX_URL_LENGTH 2084 // Maximum URL Length in Chrome: 2083 -using json = nlohmann::ordered_json; +using json = common_json; using namespace common_arg_utils; static std::initializer_list mmproj_examples = { diff --git a/common/chat-auto-parser-generator.cpp b/common/chat-auto-parser-generator.cpp index af84ff323..d7e117e4d 100644 --- a/common/chat-auto-parser-generator.cpp +++ b/common/chat-auto-parser-generator.cpp @@ -5,13 +5,12 @@ #include "common.h" #include "json-schema-to-grammar.h" #include "log.h" -#include "nlohmann/json.hpp" #include "peg-parser.h" #include #include -using json = nlohmann::ordered_json; +using json = common_json; // Helper to iterate over tools/functions static void foreach_function(const json & tools, const std::function & fn) { @@ -391,7 +390,7 @@ common_peg_parser analyze_tools::build_tool_parser_tag_tagged(parser_build_conte std::set required; if (params.contains("required")) { - params.at("required").get_to(required); + required = params.at("required").get>(); } auto schema_info = common_schema_info(); diff --git a/common/chat-auto-parser-helpers.cpp b/common/chat-auto-parser-helpers.cpp index 81b17e5e1..b37906bdf 100644 --- a/common/chat-auto-parser-helpers.cpp +++ b/common/chat-auto-parser-helpers.cpp @@ -4,14 +4,11 @@ #include "chat-peg-parser.h" #include "chat.h" #include "log.h" -#include "nlohmann/json.hpp" #include "peg-parser.h" #include #include -using json = nlohmann::ordered_json; - std::string trim_whitespace(const std::string & str) { size_t start = 0; while (start < str.length() && std::isspace(static_cast(str[start]))) { diff --git a/common/chat-auto-parser.h b/common/chat-auto-parser.h index 074216b11..8ae15c91e 100644 --- a/common/chat-auto-parser.h +++ b/common/chat-auto-parser.h @@ -4,7 +4,7 @@ #include "common.h" #include "jinja/caps.h" #include "peg-parser.h" -#include "nlohmann/json.hpp" +#include "json.h" #include #include @@ -12,7 +12,7 @@ #include #include -using json = nlohmann::ordered_json; +using json = common_json; class common_chat_peg_builder; diff --git a/common/chat-diff-analyzer.cpp b/common/chat-diff-analyzer.cpp index d6d2af2d5..a7e370578 100644 --- a/common/chat-diff-analyzer.cpp +++ b/common/chat-diff-analyzer.cpp @@ -4,11 +4,11 @@ #include "chat.h" #include "common.h" #include "log.h" -#include "nlohmann/json.hpp" #include "peg-parser.h" #include #include +#include #include #include @@ -17,7 +17,7 @@ #define ANSI_ORANGE "\033[1m\x1b[38;5;214m" #define ANSI_RED "\033[1m\x1b[38;5;196m" -using json = nlohmann::ordered_json; +using json = common_json; namespace autoparser { @@ -929,7 +929,7 @@ void analyze_tools::analyze_tool_call_format_json_native(const std::string & cle int json_end = clean_haystack.find_last_of('}'); std::string cut = clean_haystack.substr(json_start, json_end - json_start + 1); json call_struct = json::parse(cut); - auto register_field = [&](const std::string & prefix, const nlohmann::detail::iteration_proxy_value & subel) { + auto register_field = [&](const std::string & prefix, const common_json_entry & subel) { if (subel.value().is_string() && std::string(subel.value()).find("call0000") != std::string::npos) { format.id_field = !prefix.empty() ? prefix + "." + subel.key() : subel.key(); } else if (subel.value().is_string() && std::string(subel.value()) == fun_name_needle) { diff --git a/common/chat-peg-parser.cpp b/common/chat-peg-parser.cpp index 06737b165..79b97a80f 100644 --- a/common/chat-peg-parser.cpp +++ b/common/chat-peg-parser.cpp @@ -4,12 +4,10 @@ #include "ggml.h" #include "peg-parser.h" -#include - #include #include -using ordered_json = nlohmann::ordered_json; +using ordered_json = common_json; static std::string_view trim_trailing_space(std::string_view sv, int max = -1) { int count = 0; diff --git a/common/chat-peg-parser.h b/common/chat-peg-parser.h index 5d764dbaa..114fa049f 100644 --- a/common/chat-peg-parser.h +++ b/common/chat-peg-parser.h @@ -128,7 +128,7 @@ class common_chat_peg_builder : public common_peg_parser_builder { // parameters_order: order in which JSON fields should be parsed common_peg_parser standard_json_tools(const std::string & section_start, const std::string & section_end, - const nlohmann::ordered_json & tools, + const common_json & tools, bool parallel_tool_calls, bool force_tool_calls, const std::string & name_key = "", @@ -143,13 +143,13 @@ class common_chat_peg_builder : public common_peg_parser_builder { // Legacy-compatible helper for building XML/tagged style tool calls // Used by tests and manual parsers common_peg_parser standard_constructed_tools(const std::map & markers, - const nlohmann::ordered_json & tools, + const common_json & tools, bool parallel_tool_calls, bool force_tool_calls); // Helper for Python-style function call format: name(arg1="value1", arg2=123) // Used by LFM2 and similar templates - common_peg_parser python_style_tool_calls(const nlohmann::ordered_json & tools, + common_peg_parser python_style_tool_calls(const common_json & tools, bool parallel_tool_calls, bool allow_json_literals); @@ -158,19 +158,19 @@ class common_chat_peg_builder : public common_peg_parser_builder { common_peg_parser python_or_json_value(); // Implementation helpers for standard_json_tools — one per JSON tool call layout mode - common_peg_parser build_json_tools_function_is_key(const nlohmann::ordered_json & tools, + common_peg_parser build_json_tools_function_is_key(const common_json & tools, const std::string & args_key, const std::string & effective_args_key, const std::string & call_id_key, const std::string & gen_call_id_key); - common_peg_parser build_json_tools_nested_keys(const nlohmann::ordered_json & tools, + common_peg_parser build_json_tools_nested_keys(const common_json & tools, const std::string & effective_name_key, const std::string & effective_args_key, const std::string & call_id_key, const std::string & gen_call_id_key); - common_peg_parser build_json_tools_flat_keys(const nlohmann::ordered_json & tools, + common_peg_parser build_json_tools_flat_keys(const common_json & tools, const std::string & effective_name_key, const std::string & effective_args_key, const std::string & call_id_key, diff --git a/common/chat.cpp b/common/chat.cpp index 39761f12a..24618d35a 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -6,6 +6,7 @@ #include "common.h" #include "ggml.h" #include "json-schema-to-grammar.h" +#include "json.h" #include "log.h" #include "jinja/value.h" @@ -13,14 +14,13 @@ #include "jinja/caps.h" #include "peg-parser.h" -#include "nlohmann/json.hpp" - #include #include #include #include #include #include +#include #include #include @@ -30,7 +30,7 @@ #include #include -using json = nlohmann::ordered_json; +using json = common_json; static std::string format_time(const std::chrono::system_clock::time_point & now, const std::string & format) { auto time = std::chrono::system_clock::to_time_t(now); @@ -48,7 +48,7 @@ static json safe_args_parse(const std::string & to_parse) { } try { return json::parse(stripped); - } catch (json::exception & e) { + } catch (const common_json_error & e) { return stripped; } } @@ -488,17 +488,17 @@ struct messages_inp_normalizer { json normalized = json::array(); for (const auto & msg : messages) { json copy = msg; - auto it = copy.find("content"); - if (it != copy.end()) { - if (only_typed && it->is_string()) { - *it = json::array({ + if (copy.contains("content")) { + json & it = copy.at("content"); + if (only_typed && it.is_string()) { + it = json::array({ json{ {"type", "text"}, - {"text", it->get()}, + {"text", it.get()}, } }); - } else if (only_string && it->is_array()) { - *it = concat_content_parts(*it); + } else if (only_string && it.is_array()) { + it = concat_content_parts(it); } } normalized.push_back(std::move(copy)); @@ -608,7 +608,7 @@ std::vector common_chat_tools_parse_oaicompat(const json & too return result; } -common_chat_continuation common_chat_continuation_parse(const nlohmann::ordered_json & value) { +common_chat_continuation common_chat_continuation_parse(const common_json & value) { if (value.is_boolean() && value.get()) { return COMMON_CHAT_CONTINUATION_AUTO; } @@ -920,7 +920,7 @@ static void foreach_parameter(const json & const auto & props = params.at("properties"); std::set required; if (params.contains("required") && params.at("required").is_array()) { - params.at("required").get_to(required); + required = params.at("required").get>(); } for (const auto & [name, prop] : props.items()) { bool is_required = (required.find(name) != required.end()); @@ -937,7 +937,7 @@ static std::string common_chat_template_direct_apply_impl( jinja::context ctx(tmpl.source()); // messages_override is already built for this template, do not touch its content parts - nlohmann::ordered_json inp = nlohmann::ordered_json{ + json inp = json{ {"messages", messages_override.has_value() ? *messages_override : messages_inp_normalizer(tmpl.original_caps()).normalize(inputs.messages)}, @@ -1058,7 +1058,7 @@ static common_chat_params common_chat_params_init_ministral_3(const common_chat_ }); } else if (msg.at("content").is_array()) { auto blocks = msg.at("content"); - content.insert(content.end(), blocks.begin(), blocks.end()); + content.insert(blocks); } } @@ -2238,7 +2238,7 @@ static common_chat_params common_chat_params_init_deepseek_v3_2(const common_cha std::set required; if (params.contains("required")) { - params.at("required").get_to(required); + required = params.at("required").get>(); } auto schema_info = common_schema_info(); @@ -2860,7 +2860,7 @@ static common_chat_params common_chat_params_init_minimax_m3(const common_chat_t std::set required; if (schema.contains("required")) { - schema.at("required").get_to(required); + required = schema.at("required").get>(); } std::vector required_elements; @@ -2972,10 +2972,10 @@ static void system_message_not_supported(json & messages) { auto & second_msg = messages[1]; second_msg["content"] = first_msg.at("content").get() + "\n" + second_msg.at("content").get(); - messages.erase(messages.begin()); + messages.erase(0); } else { LOG_WRN("Removing system prompt due to template not supporting system role\n"); - messages.erase(messages.begin()); + messages.erase(0); } } } diff --git a/common/chat.h b/common/chat.h index 6d5b220ae..cb39e3458 100644 --- a/common/chat.h +++ b/common/chat.h @@ -8,7 +8,7 @@ #include "jinja/runtime.h" #include "jinja/caps.h" -#include "nlohmann/json_fwd.hpp" +#include "json.h" #include #include @@ -17,7 +17,6 @@ #include using chat_template_caps = jinja::caps; -using json = nlohmann::ordered_json; struct common_chat_templates; @@ -87,7 +86,7 @@ struct common_chat_msg { std::string tool_name; std::string tool_call_id; - nlohmann::ordered_json to_json_oaicompat(bool concat_typed_text = false) const; + common_json to_json_oaicompat(bool concat_typed_text = false) const; std::string render_content(const std::string & delimiter = "\n\n") const; @@ -211,7 +210,7 @@ struct common_chat_msg_delimiters { // split tokens into message spans. skips maps a start index to a length of a region to jump over without matching common_chat_msg_spans split(const llama_tokens & tokens, const std::map & skips = {}) const; - nlohmann::ordered_json to_json() const; + common_json to_json() const; }; struct common_chat_tool { @@ -350,16 +349,16 @@ common_chat_tool_choice common_chat_tool_choice_parse_oaicompat(const std::strin bool common_chat_templates_support_enable_thinking(const common_chat_templates * chat_templates); // Parses a JSON array of messages in OpenAI's chat completion API format. -std::vector common_chat_msgs_parse_oaicompat(const nlohmann::ordered_json & messages); +std::vector common_chat_msgs_parse_oaicompat(const common_json & messages); -std::vector common_chat_tools_parse_oaicompat(const nlohmann::ordered_json & tools); +std::vector common_chat_tools_parse_oaicompat(const common_json & tools); -common_chat_continuation common_chat_continuation_parse(const nlohmann::ordered_json & value); +common_chat_continuation common_chat_continuation_parse(const common_json & value); // DEPRECATED: only used in tests -nlohmann::ordered_json common_chat_msgs_to_json_oaicompat(const std::vector & msgs, bool concat_typed_text = false); +common_json common_chat_msgs_to_json_oaicompat(const std::vector & msgs, bool concat_typed_text = false); -nlohmann::ordered_json common_chat_tools_to_json_oaicompat(const std::vector & tools); +common_json common_chat_tools_to_json_oaicompat(const std::vector & tools); // get template caps, useful for reporting to server /props endpoint std::map common_chat_templates_get_caps(const common_chat_templates * chat_templates); @@ -386,4 +385,4 @@ struct common_chat_prompt_preset { common_chat_prompt_preset common_chat_get_asr_prompt(const common_chat_templates * chat_templates); -common_chat_msg_delimiters common_chat_msg_delimiters_parse(const nlohmann::ordered_json & delimiters); +common_chat_msg_delimiters common_chat_msg_delimiters_parse(const common_json & delimiters); diff --git a/common/download.cpp b/common/download.cpp index 2509f75ab..4b28a708c 100644 --- a/common/download.cpp +++ b/common/download.cpp @@ -5,9 +5,7 @@ #include "log.h" #include "download.h" #include "hf-cache.h" - -#define JSON_ASSERT GGML_ASSERT -#include +#include "json.h" #include #include @@ -44,8 +42,6 @@ #include #endif -using json = nlohmann::ordered_json; - // // downloader // @@ -856,8 +852,8 @@ static std::string common_docker_get_token(const std::string & repo) { throw std::runtime_error("Failed to get Docker registry token, HTTP code: " + std::to_string(res.first)); } - std::string response_str(res.second.begin(), res.second.end()); - nlohmann::ordered_json response = nlohmann::ordered_json::parse(response_str); + std::string response_str(res.second.begin(), res.second.end()); + common_json response = common_json::parse(response_str); if (!response.contains("token")) { throw std::runtime_error("Docker registry token response missing 'token' field"); @@ -919,9 +915,9 @@ std::string common_docker_resolve_model(const std::string & docker) { throw std::runtime_error("Failed to get Docker manifest, HTTP code: " + std::to_string(manifest_res.first)); } - std::string manifest_str(manifest_res.second.begin(), manifest_res.second.end()); - nlohmann::ordered_json manifest = nlohmann::ordered_json::parse(manifest_str); - std::string gguf_digest; // Find the GGUF layer + std::string manifest_str(manifest_res.second.begin(), manifest_res.second.end()); + common_json manifest = common_json::parse(manifest_str); + std::string gguf_digest; // Find the GGUF layer if (manifest.contains("layers")) { for (const auto & layer : manifest["layers"]) { if (layer.contains("mediaType")) { diff --git a/common/hf-cache.cpp b/common/hf-cache.cpp index f1dacaa47..50d6dd610 100644 --- a/common/hf-cache.cpp +++ b/common/hf-cache.cpp @@ -4,9 +4,7 @@ #include "common.h" #include "log.h" #include "http.h" - -#define JSON_ASSERT GGML_ASSERT -#include +#include "json.h" #include #include @@ -15,8 +13,6 @@ #include #include -namespace nl = nlohmann; - #if defined(_WIN32) #define WIN32_LEAN_AND_MEAN #ifndef NOMINMAX @@ -195,8 +191,8 @@ static void safe_write_file(const fs::path & path, const std::string & data) { } } -static nl::json api_get(const std::string & url, - const std::string & token) { +static common_json api_get(const std::string & url, + const std::string & token) { auto [cli, parts] = common_http_client(url); httplib::Headers headers = { @@ -214,10 +210,10 @@ static nl::json api_get(const std::string & url, auto body = res->body; if (res->status == 200) { - return nl::json::parse(res->body); + return common_json::parse(res->body); } try { - body = nl::json::parse(res->body)["error"].get(); + body = common_json::parse(res->body)["error"].get(); } catch (...) { } throw std::runtime_error("GET failed (" + std::to_string(res->status) + "): " + body); @@ -280,7 +276,7 @@ static std::string get_repo_commit(const std::string & repo_id, safe_write_file(refs_path / name, commit); return commit; - } catch (const nl::json::exception & e) { + } catch (const common_json_error & e) { LOG_ERR("%s: JSON error: %s\n", __func__, e.what()); } catch (const std::exception & e) { LOG_ERR("%s: error: %s\n", __func__, e.what()); @@ -358,7 +354,7 @@ hf_files get_repo_files(const std::string & repo_id, files.push_back(file); } - } catch (const nl::json::exception & e) { + } catch (const common_json_error & e) { LOG_ERR("%s: JSON error: %s\n", __func__, e.what()); } catch (const std::exception & e) { LOG_ERR("%s: error: %s\n", __func__, e.what()); diff --git a/common/jinja/README.md b/common/jinja/README.md index 829124076..5b97fc92c 100644 --- a/common/jinja/README.md +++ b/common/jinja/README.md @@ -7,7 +7,7 @@ The implementation can be found in the `common/jinja` directory. ## Key Features - Input marking: security against special token injection -- Decoupled from `nlohmann::json`: this dependency is only used for JSON-to-internal type translation and is completely optional +- Decoupled from the JSON library: `common_json` is only used for JSON-to-internal type translation and is completely optional - Minimal primitive types: int, float, bool, string, array, object, none, undefined - Detailed logging: allow source tracing on error - Clean architecture: workarounds are applied to input data before entering the runtime (see `common/chat.cpp`) diff --git a/common/jinja/caps.cpp b/common/jinja/caps.cpp index 6e3a1e9b2..9971c021e 100644 --- a/common/jinja/caps.cpp +++ b/common/jinja/caps.cpp @@ -4,14 +4,14 @@ // note: the json dependency is only for defining input in a convenient way // we can remove it in the future when we figure out a better way to define inputs using jinja::value -#include +#include "json.h" #include #include #define FILENAME "jinja-caps" -using json = nlohmann::ordered_json; +using json = common_json; namespace jinja { diff --git a/common/jinja/value.cpp b/common/jinja/value.cpp index 870596d61..6999ef7d6 100644 --- a/common/jinja/value.cpp +++ b/common/jinja/value.cpp @@ -3,7 +3,7 @@ #include "value.h" // for converting from JSON to jinja values -#include +#include "json.h" #include #include @@ -1355,7 +1355,7 @@ const func_builtins & value_undefined_t::get_builtins() const { ////////////////////////////////// -static value from_json(const nlohmann::ordered_json & j, bool mark_input) { +static value from_json(const common_json & j, bool mark_input) { if (j.is_null()) { return mk_val(); } else if (j.is_boolean()) { @@ -1452,7 +1452,7 @@ bool value_compare(const value & a, const value & b, value_compare_op op) { } template<> -void global_from_json(context & ctx, const nlohmann::ordered_json & json_obj, bool mark_input) { +void global_from_json(context & ctx, const common_json & json_obj, bool mark_input) { // printf("global_from_json: %s\n" , json_obj.dump(2).c_str()); if (json_obj.is_null() || !json_obj.is_object()) { throw std::runtime_error("global_from_json: input JSON value must be an object"); diff --git a/common/jinja/value.h b/common/jinja/value.h index 5cf85e4f5..4926fb680 100644 --- a/common/jinja/value.h +++ b/common/jinja/value.h @@ -86,7 +86,7 @@ struct context; // forward declaration // marking input can be useful for tracking data provenance // and preventing template injection attacks // -// Note: T_JSON can be nlohmann::ordered_json +// Note: T_JSON can be common_json template void global_from_json(context & ctx, const T_JSON & json_obj, bool mark_input); diff --git a/common/json-schema-to-grammar.cpp b/common/json-schema-to-grammar.cpp index 955b4e014..0aee51b26 100644 --- a/common/json-schema-to-grammar.cpp +++ b/common/json-schema-to-grammar.cpp @@ -1,9 +1,8 @@ #include "json-schema-to-grammar.h" #include "common.h" -#include - #include +#include #include #include #include @@ -12,7 +11,7 @@ #include #include -using json = nlohmann::ordered_json; +using json = common_json; static std::string build_repetition(const std::string & item_rule, int min_items, int max_items, const std::string & separator_rule = "") { auto has_max = max_items != std::numeric_limits::max(); @@ -917,7 +916,11 @@ public: return _add_rule(rule_name, _resolve_ref(schema["$ref"])); } if (schema.contains("oneOf") || schema.contains("anyOf")) { - std::vector alt_schemas = schema.contains("oneOf") ? schema["oneOf"].get>() : schema["anyOf"].get>(); + const json & alts = schema.contains("oneOf") ? schema.at("oneOf") : schema.at("anyOf"); + std::vector alt_schemas; + for (const auto & alt : alts) { + alt_schemas.push_back(alt); + } return _add_rule(rule_name, _generate_union_rule(name, alt_schemas)); } if (schema_type.is_array()) { @@ -1111,7 +1114,7 @@ common_schema_info::~common_schema_info() = default; common_schema_info::common_schema_info(common_schema_info &&) noexcept = default; common_schema_info & common_schema_info::operator=(common_schema_info &&) noexcept = default; -void common_schema_info::resolve_refs(nlohmann::ordered_json & schema) { +void common_schema_info::resolve_refs(common_json & schema) { impl_->resolve_refs(schema, ""); } @@ -1119,7 +1122,7 @@ void common_schema_info::resolve_refs(nlohmann::ordered_json & schema) { // Some models emit raw string values rather than JSON-encoded strings for string parameters. // If any branch of the schema (via oneOf, anyOf, $ref, etc.) permits a string, this returns // true, allowing callers to handle the value as a raw string for simplicity. -bool common_schema_info::resolves_to_string(const nlohmann::ordered_json & schema) { +bool common_schema_info::resolves_to_string(const common_json & schema) { std::unordered_set visited_refs; std::function check = [&](const json & s) -> bool { @@ -1227,7 +1230,7 @@ bool common_schema_info::resolves_to_string(const nlohmann::ordered_json & schem return check(schema); } -std::string json_schema_to_grammar(const json & schema, bool force_gbnf) { +std::string json_schema_to_grammar(const common_json & schema, bool force_gbnf) { #ifdef LLAMA_USE_LLGUIDANCE if (!force_gbnf) { return "%llguidance {}\nstart: %json " + schema.dump(); @@ -1248,10 +1251,10 @@ std::string build_grammar(const std::function +#include "json.h" #include #include #include -std::string json_schema_to_grammar(const nlohmann::ordered_json & schema, +std::string json_schema_to_grammar(const common_json & schema, bool force_gbnf = false); class common_schema_converter; @@ -24,14 +24,14 @@ class common_schema_info { common_schema_info(common_schema_info &&) noexcept; common_schema_info & operator=(common_schema_info &&) noexcept; - void resolve_refs(nlohmann::ordered_json & schema); - bool resolves_to_string(const nlohmann::ordered_json & schema); + void resolve_refs(common_json & schema); + bool resolves_to_string(const common_json & schema); }; struct common_grammar_builder { std::function add_rule; - std::function add_schema; - std::function resolve_refs; + std::function add_schema; + std::function resolve_refs; }; struct common_grammar_options { diff --git a/common/json.cpp b/common/json.cpp new file mode 100644 index 000000000..547542bb7 --- /dev/null +++ b/common/json.cpp @@ -0,0 +1,437 @@ +#include "json.h" + +#include "ggml.h" + +#define JSON_ASSERT GGML_ASSERT +#include + +#include +#include +#include +#include +#include + +using nlohmann::ordered_json; + +// a common_json is the backing value, so any value of a tree can be used as a common_json +static_assert(sizeof(ordered_json) <= sizeof(common_json), "common_json storage is too small"); +static_assert(alignof(ordered_json) <= alignof(common_json), "common_json alignment is too weak"); + +// runs fn and gives every error of the backing library as a common_json_error +template +static decltype(auto) guard(F && fn) { + try { + return fn(); + } catch (const ordered_json::exception & e) { + throw common_json_error(e.what()); + } +} + +static ordered_json & as_json(common_json * self) { + return *reinterpret_cast(self); +} + +static const ordered_json & as_json(const common_json * self) { + return *reinterpret_cast(self); +} + +static common_json & as_common(ordered_json & json) { + return *reinterpret_cast(&json); +} + +static const common_json & as_common(const ordered_json & json) { + return *reinterpret_cast(&json); +} + +static ordered_json to_json(const common_json_value & val) { + switch (val.type) { + case common_json_value::VAL_NULL: return nullptr; + case common_json_value::VAL_BOOL: return val.val_bool; + case common_json_value::VAL_INT: return val.val_int; + case common_json_value::VAL_UINT: return val.val_uint; + case common_json_value::VAL_DOUBLE: return val.val_double; + case common_json_value::VAL_STRING: return val.val_string; + case common_json_value::VAL_JSON: + // one owner means no one else can see this tree, so it is safe to move it out + // note: this makes a value single use, same as the json_ref of the backing library + if (val.val_json.use_count() == 1) { + return std::move(as_json(val.val_json.get())); + } + return as_json(val.val_json.get()); + } + + return nullptr; +} + +common_json_value::common_json_value(const char * val) { + if (val) { + type = VAL_STRING; + val_string = val; + } else { + type = VAL_NULL; + } +} + +common_json_value::common_json_value(const common_json & val) : + type(VAL_JSON), val_json(std::make_shared(val)) {} + +common_json_value::common_json_value(common_json && val) : + type(VAL_JSON), val_json(std::make_shared(std::move(val))) {} + +template +common_json_value::common_json_value(const std::set & vals) : type(VAL_JSON) { + common_json out = common_json::array(); + + for (const auto & val : vals) { + out.push_back(val); + } + + val_json = std::make_shared(std::move(out)); +} + +// a set value is usable only for the types below +#define COMMON_JSON_SET(...) template common_json_value::common_json_value(const std::set<__VA_ARGS__> &); + +COMMON_JSON_SET(int) +COMMON_JSON_SET(std::string) + +#undef COMMON_JSON_SET + +template +common_json_value::common_json_value(const std::map & vals) : type(VAL_JSON) { + common_json out = common_json::object(); + + for (const auto & val : vals) { + out.set({ val.first, val.second }); + } + + val_json = std::make_shared(std::move(out)); +} + +// a map value is usable only for the types below +#define COMMON_JSON_MAP(...) template common_json_value::common_json_value(const std::map &); + +COMMON_JSON_MAP(bool) +COMMON_JSON_MAP(std::string) + +#undef COMMON_JSON_MAP + +template +common_json_value::common_json_value(const std::unordered_map & vals) : type(VAL_JSON) { + common_json out = common_json::object(); + + for (const auto & val : vals) { + out.set({ val.first, val.second }); + } + + val_json = std::make_shared(std::move(out)); +} + +// an unordered map value is usable only for the types below +#define COMMON_JSON_UMAP(...) template common_json_value::common_json_value(const std::unordered_map &); + +COMMON_JSON_UMAP(size_t) + +#undef COMMON_JSON_UMAP + +template +common_json_value::common_json_value(const std::vector & vals) : type(VAL_JSON) { + common_json out = common_json::array(); + + for (const auto & val : vals) { + out.push_back(val); + } + + val_json = std::make_shared(std::move(out)); +} + +// a vector value is usable only for the types below +// note: std::vector is not here, its proxy reference does not convert +#define COMMON_JSON_VEC(...) template common_json_value::common_json_value(const std::vector<__VA_ARGS__> &); + +COMMON_JSON_VEC(int) +COMMON_JSON_VEC(unsigned char) +COMMON_JSON_VEC(unsigned int) +COMMON_JSON_VEC(long) +COMMON_JSON_VEC(unsigned long) +COMMON_JSON_VEC(long long) +COMMON_JSON_VEC(unsigned long long) +COMMON_JSON_VEC(float) +COMMON_JSON_VEC(double) +COMMON_JSON_VEC(std::string) +COMMON_JSON_VEC(std::vector) +COMMON_JSON_VEC(common_json) + +#undef COMMON_JSON_VEC + +common_json_value::common_json_value(std::initializer_list items) : + type(VAL_JSON), val_json(std::make_shared(items)) {} + +// null, same as the backing library +// operator[] turns it into an object, push_back() into an array +common_json::common_json() { + new (storage) ordered_json(); +} + +common_json::common_json(const common_json & other) { + new (storage) ordered_json(as_json(&other)); +} + +common_json::common_json(common_json && other) noexcept { + new (storage) ordered_json(std::move(as_json(&other))); +} + +common_json::common_json(std::initializer_list items) { + new (storage) ordered_json(ordered_json::object()); + + for (const auto & item : items) { + set(item); + } +} + +common_json::common_json(const common_json_value & val) { + new (storage) ordered_json(to_json(val)); +} + +common_json::common_json(std::nullptr_t) { + new (storage) ordered_json(nullptr); +} + +common_json & common_json::operator=(common_json other) noexcept { + as_json(this).swap(as_json(&other)); + + return *this; +} + +common_json::~common_json() { + as_json(this).~basic_json(); +} + +common_json common_json::parse(const std::string & text) { + try { + // the assignment moves the parsed tree in, it does not copy + common_json out; + as_json(&out) = ordered_json::parse(text); + return out; + } catch (const std::exception & e) { + throw common_json_error(e.what()); + } +} + +common_json common_json::parse_no_throw(const std::string & text) { + common_json out; + as_json(&out) = ordered_json::parse(text, nullptr, false); + return out; +} + +bool common_json::is_discarded() const { + return as_json(this).is_discarded(); +} + +common_json common_json::array() { + common_json out; + as_json(&out) = ordered_json::array(); + return out; +} + +common_json common_json::array(std::initializer_list vals) { + common_json out; + ordered_json & arr = as_json(&out); + arr = ordered_json::array(); + + for (const auto & val : vals) { + arr.push_back(to_json(val)); + } + + return out; +} + +common_json common_json::object() { + common_json out; + as_json(&out) = ordered_json::object(); + return out; +} + +common_json common_json::object(std::initializer_list items) { + return common_json(items); +} + +common_json common_json::make(const common_json_value & val) { + return common_json(val); +} + +bool common_json::is_null() const { return as_json(this).is_null(); } +bool common_json::is_object() const { return as_json(this).is_object(); } +bool common_json::is_array() const { return as_json(this).is_array(); } +bool common_json::is_string() const { return as_json(this).is_string(); } +bool common_json::is_boolean() const { return as_json(this).is_boolean(); } +bool common_json::is_number() const { return as_json(this).is_number(); } +bool common_json::is_number_integer() const { return as_json(this).is_number_integer(); } +bool common_json::is_number_float() const { return as_json(this).is_number_float(); } + +bool common_json::empty() const { return as_json(this).empty(); } +size_t common_json::size() const { return as_json(this).size(); } + +bool common_json::contains(const std::string & key) const { + return as_json(this).contains(key); +} + +bool common_json::operator==(const common_json_value & val) const { + // compare a tree in place, to_json() would copy it + if (val.type == common_json_value::VAL_JSON) { + return as_json(this) == as_json(val.val_json.get()); + } + return as_json(this) == to_json(val); +} + +bool common_json::operator!=(const common_json_value & val) const { + return !(*this == val); +} + +common_json & common_json::at(const std::string & key) { return guard([&]() -> common_json & { return as_common(as_json(this).at(key)); }); } +const common_json & common_json::at(const std::string & key) const { return guard([&]() -> const common_json & { return as_common(as_json(this).at(key)); }); } +common_json & common_json::at(size_t idx) { return guard([&]() -> common_json & { return as_common(as_json(this).at(idx)); }); } +const common_json & common_json::at(size_t idx) const { return guard([&]() -> const common_json & { return as_common(as_json(this).at(idx)); }); } + +common_json & common_json::operator[](const std::string & key) { return guard([&]() -> common_json & { return as_common(as_json(this)[key]); }); } +const common_json & common_json::operator[](const std::string & key) const { return guard([&]() -> const common_json & { return as_common(as_json(this).at(key)); }); } +common_json & common_json::operator[](size_t idx) { return guard([&]() -> common_json & { return as_common(as_json(this)[idx]); }); } +const common_json & common_json::operator[](size_t idx) const { return guard([&]() -> const common_json & { return as_common(as_json(this).at(idx)); }); } + +common_json & common_json::front() { return as_common(as_json(this).front()); } +const common_json & common_json::front() const { return as_common(as_json(this).front()); } +common_json & common_json::back() { return as_common(as_json(this).back()); } +const common_json & common_json::back() const { return as_common(as_json(this).back()); } + +void common_json::clear() { + as_json(this).clear(); +} + +void common_json::erase(const std::string & key) { + guard([&] { as_json(this).erase(key); }); +} + +void common_json::erase(size_t idx) { + guard([&] { as_json(this).erase(idx); }); +} + +void common_json::assign(const common_json_value & val) { + as_json(this) = to_json(val); +} + +void common_json::set(const common_json_item & item) { + guard([&] { as_json(this)[item.key] = to_json(item.val); }); +} + +void common_json::push_back(const common_json_value & val) { + guard([&] { as_json(this).push_back(to_json(val)); }); +} + +void common_json::push_back(std::initializer_list items) { + common_json val(items); + + guard([&] { as_json(this).push_back(std::move(as_json(&val))); }); +} + +size_t common_json::count(const std::string & key) const { + return as_json(this).count(key); +} + +void common_json::insert(const common_json & vals) { + guard([&] { + ordered_json & self = as_json(this); + + self.insert(self.end(), as_json(&vals).begin(), as_json(&vals).end()); + }); +} + +std::string common_json::dump(int indent) const { + return guard([&] { return as_json(this).dump(indent); }); +} + +std::string common_json::dump_safe(int indent) const { + return as_json(this).dump(indent, ' ', false, ordered_json::error_handler_t::replace); +} + +// an array is indexed directly, an object needs a walk from the start +common_json & common_json::iterator::operator*() const { + return guard([&]() -> common_json & { + ordered_json & j = as_json(node); + + if (j.is_object()) { + return as_common(std::next(j.begin(), idx).value()); + } + if (j.is_array()) { + return as_common(j[idx]); + } + + // a plain value gives itself once, same as the backing library + return *node; + }); +} + +std::string common_json::iterator::key() const { + return guard([&] { return std::next(as_json(node).begin(), idx).key(); }); +} + +common_json::iterator common_json::begin() const { + return iterator(const_cast(this), 0); +} + +common_json::iterator common_json::end() const { + return iterator(const_cast(this), size()); +} + +// the keys follow the backing library: the index for an array, "" for a plain value +common_json::items_view::entry common_json::items_view::iterator::operator*() const { + return guard([&]() -> entry { + ordered_json & j = as_json(node); + + if (j.is_object()) { + auto it = std::next(j.begin(), idx); + + return { it.key(), as_common(it.value()) }; + } + if (j.is_array()) { + return { std::to_string(idx), as_common(j[idx]) }; + } + + return { std::string(), *node }; + }); +} + +common_json::items_view common_json::items() const { + return items_view(const_cast(this), size()); +} + +template T common_json::get() const { + return guard([&] { return as_json(this).get(); }); +} + +// the backing library cannot build a common_json, so this one is just a copy +template <> common_json common_json::get() const { + return *this; +} + +// get() is usable only for the types below + +#define COMMON_JSON_GET(...) template __VA_ARGS__ common_json::get<__VA_ARGS__>() const; + +COMMON_JSON_GET(bool) +COMMON_JSON_GET(int) +COMMON_JSON_GET(unsigned int) +COMMON_JSON_GET(long) +COMMON_JSON_GET(unsigned long) +COMMON_JSON_GET(long long) +COMMON_JSON_GET(unsigned long long) +COMMON_JSON_GET(float) +COMMON_JSON_GET(double) +COMMON_JSON_GET(std::string) +COMMON_JSON_GET(std::vector) +COMMON_JSON_GET(std::vector) +COMMON_JSON_GET(std::set) +COMMON_JSON_GET(std::vector) +COMMON_JSON_GET(std::vector) +COMMON_JSON_GET(std::unordered_map) + +#undef COMMON_JSON_GET diff --git a/common/json.h b/common/json.h new file mode 100644 index 000000000..9e20a2adb --- /dev/null +++ b/common/json.h @@ -0,0 +1,354 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// common_json, a thin wrapper around vendor json library +// the underlay library is pimpl, we are using nlohmann::json for now +// +// many features of the library are deliberately left out, to keep this interface small and generic and to keep compile time down +// +// some main differences compared to nlohmann::json : +// - object keys keep the order in which they are added +// - errors are always throw as common_json_error +// - obj.push_back({key, val}) is intentionally unsupported to avoid confusion with push_back on a vector; write it as obj[key] = val for clarity +// - a braced pair in value position does not build, e.g. {"key", {"a", "b"}}; write array({"a", "b"}) where nlohmann made an array +// +// in doubt, search the code base for an existing usage example; do not add anything to this header unless absolutely necessary + +class common_json; + +// common_json_value holds a list of these, and each of them holds a value, so one must come first +struct common_json_item; + +struct common_json_error : std::runtime_error { + using std::runtime_error::runtime_error; +}; + +// one value, tagged so that this header stays free of the backing library +// note: a value that holds a tree is single use, the second use gives null +struct common_json_value { + enum value_type { + VAL_NULL, + VAL_BOOL, + VAL_INT, + VAL_UINT, + VAL_DOUBLE, + VAL_STRING, + VAL_JSON, + }; + + value_type type = VAL_NULL; + + union { + bool val_bool; + int64_t val_int; + uint64_t val_uint = 0; + double val_double; + }; + + std::string val_string; + std::shared_ptr val_json; + + common_json_value(std::nullptr_t = nullptr) : type(VAL_NULL) {} + common_json_value(bool val) : type(VAL_BOOL), val_bool(val) {} + common_json_value(std::string val) : type(VAL_STRING), val_string(std::move(val)) {} + // without this a string_view lands on the common_json ctor below and recurses + common_json_value(std::string_view val) : type(VAL_STRING), val_string(val) {} + common_json_value(const char * val); + common_json_value(const common_json & val); + common_json_value(common_json && val); + // only for the types instantiated in json.cpp, the rest fails at link time + template common_json_value(const std::vector & vals); + // a set becomes an array, in the set's own order + template common_json_value(const std::set & vals); + // a map becomes an object, keyed in the map's own order + template common_json_value(const std::map & vals); + template common_json_value(const std::unordered_map & vals); + + // nested object, e.g. {"fn", {{"name", "x"}}} + // note: a nested pair {"a", "b"} does not build, use common_json::array({"a", "b"}) for an array + common_json_value(std::initializer_list items); + + template ::value && !std::is_same::value, int>::type = 0> + common_json_value(T val) : type(std::is_signed::value ? VAL_INT : VAL_UINT) { + if (std::is_signed::value) { + val_int = (int64_t) val; + } else { + val_uint = (uint64_t) val; + } + } + + template ::value, int>::type = 0> + common_json_value(T val) : type(VAL_DOUBLE), val_double((double) val) {} +}; + +struct common_json_item { + std::string key; + common_json_value val; + + template + common_json_item(std::string key, T && val) : + key(std::move(key)), val(std::forward(val)) {} + + // a braced list cannot deduce T, so it needs its own overload + common_json_item(std::string key, std::initializer_list items) : + key(std::move(key)), val(items) {} +}; + +// the types common_json_value holds on its own +// anything else reaches its common_json ctor and recurses forever +template struct common_json_is_value : std::integral_constant::value || + std::is_same::value || + std::is_same::value || + std::is_same::value || + std::is_same::value || + std::is_same::value || + std::is_same::value> {}; + +template +struct common_json_is_value> : std::true_type {}; + +template +struct common_json_is_value> : std::true_type {}; + +template +struct common_json_is_value> : std::true_type {}; + +template +struct common_json_is_value> : std::true_type {}; + +class common_json { + public: + common_json(); + common_json(const common_json & other); + common_json(common_json && other) noexcept; + common_json(std::initializer_list items); + common_json(const common_json_value & val); + + // direct, a value would need two conversions in a row + common_json(std::nullptr_t); + + // one step, so that "abc" or a vector can go straight into a common_json + template ::type, common_json>::value && + !std::is_same::type, common_json_value>::value, int>::type = 0> + common_json(T && val) : common_json(common_json_value(std::forward(val))) { + static_assert(common_json_is_value::type>::value, + "no common_json_value ctor holds this type, add one instead of letting it recurse"); + } + + // by value, same as the backing library + // the right side is copied before the left side can invalidate it, e.g. msg["a"] = msg.at("b") + common_json & operator=(common_json other) noexcept; + + ~common_json(); + + // throws common_json_error if the text is not valid JSON + static common_json parse(const std::string & text); + + // gives a discarded value instead of throwing, check it with is_discarded() + static common_json parse_no_throw(const std::string & text); + + bool is_discarded() const; + + static common_json array(); + static common_json array(std::initializer_list vals); + static common_json object(); + static common_json object(std::initializer_list items); + + // holds a single value, e.g. make("abc").dump() gives "\"abc\"" + static common_json make(const common_json_value & val); + + bool is_null() const; + bool is_object() const; + bool is_array() const; + bool is_string() const; + bool is_boolean() const; + bool is_number() const; + bool is_number_integer() const; + bool is_number_float() const; + + bool empty() const; + size_t size() const; + + bool contains(const std::string & key) const; + + bool operator==(const common_json_value & val) const; + bool operator!=(const common_json_value & val) const; + + // at() throws common_json_error if the key is missing, operator[] adds a null value instead + // note: a const operator[] cannot add, it throws like at() + common_json & at(const std::string & key); + const common_json & at(const std::string & key) const; + common_json & at(size_t idx); + const common_json & at(size_t idx) const; + + common_json & operator[](const std::string & key); + const common_json & operator[](const std::string & key) const; + common_json & operator[](const char * key) { return (*this)[std::string(key)]; } + const common_json & operator[](const char * key) const { return (*this)[std::string(key)]; } + common_json & operator[](int idx) { return (*this)[to_idx(idx)]; } + const common_json & operator[](int idx) const { return (*this)[to_idx(idx)]; } + common_json & operator[](size_t idx); + const common_json & operator[](size_t idx) const; + + common_json & front(); + const common_json & front() const; + common_json & back(); + const common_json & back() const; + + void clear(); + + void erase(const std::string & key); + void erase(size_t idx); + + // only for the types instantiated in json.cpp, the rest fails at link time + template T get() const; + + // implicit get() for plain values, so they can be assigned to their C++ type directly + // note: kept to this short list on purpose, a wider one makes j["key"] ambiguous + // note: a numeric one would make "str = json;" ambiguous, a number converts to char too + operator std::string() const { return get(); } + + template + T value(const std::string & key, T def) const { + return contains(key) ? at(key).get() : def; + } + + std::string value(const std::string & key, const char * def) const { + return contains(key) ? at(key).get() : std::string(def); + } + + // a JSON default needs no get(), it is already the right type + common_json value(const std::string & key, const common_json & def) const { + return contains(key) ? at(key) : def; + } + + void assign(const common_json_value & val); + void set(const common_json_item & item); + void push_back(const common_json_value & val); + + // appends one object, e.g. push_back({{"a", 1}}) + void push_back(std::initializer_list items); + + // 1 if the key is there, 0 if not + size_t count(const std::string & key) const; + + // appends every value of another array; inserting an array into itself throws + void insert(const common_json & vals); + + // a common_json goes through the copy assignment above, everything else becomes a value + template ::type, common_json>::value, int>::type = 0> + common_json & operator=(T && val) { + assign(common_json_value(std::forward(val))); + return *this; + } + + std::string dump(int indent = -1) const; + + // same as dump(), but bad UTF-8 gets replaced instead of throwing + std::string dump_safe(int indent = -1) const; + + // walks an array by index, or an object in insertion order + // a plain value gives itself once, same as the backing library + class iterator { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = common_json; + using difference_type = std::ptrdiff_t; + using pointer = common_json *; + using reference = common_json &; + + iterator(common_json * node, size_t idx) : node(node), idx(idx) {} + + common_json & operator*() const; + common_json & value() const { return **this; } + std::string key() const; + + iterator & operator++() { + idx++; + return *this; + } + + bool operator!=(const iterator & other) const { return idx != other.idx; } + bool operator==(const iterator & other) const { return idx == other.idx; } + + private: + common_json * node; + size_t idx; + }; + + iterator begin() const; + iterator end() const; + + // allows: for (const auto & [key, val] : obj.items()) + class items_view { + public: + // the members are public, so an entry also works with structured bindings + struct entry { + std::string k; + common_json & v; + + const std::string & key() const { return k; } + common_json & value() const { return v; } + }; + + items_view(common_json * node, size_t n) : node(node), n(n) {} + + class iterator { + public: + iterator(common_json * node, size_t idx) : node(node), idx(idx) {} + + entry operator*() const; + + iterator & operator++() { + idx++; + return *this; + } + + bool operator!=(const iterator & other) const { return idx != other.idx; } + + private: + common_json * node; + size_t idx; + }; + + iterator begin() const { return iterator(node, 0); } + iterator end() const { return iterator(node, n); } + + private: + common_json * node; + size_t n; + }; + + items_view items() const; + + private: + // a negative index must not turn into a huge size_t + static size_t to_idx(int idx) { + if (idx < 0) { + throw common_json_error("negative array index"); + } + return (size_t) idx; + } + + // the backing value is built here, json.cpp checks that it fits + // it cannot be a pointer: a value inside a tree would then not be a common_json + // at() could then only give back a copy instead of a real reference + alignas(8) unsigned char storage[32]; +}; + +using common_json_entry = common_json::items_view::entry; diff --git a/common/peg-parser.cpp b/common/peg-parser.cpp index 4a4be7cf7..46fc29bf2 100644 --- a/common/peg-parser.cpp +++ b/common/peg-parser.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include @@ -1120,8 +1119,8 @@ common_peg_parser common_peg_parser_builder::chars(const std::string & classes, return wrap(arena_.add_parser(common_peg_chars_parser{classes, ranges, negated, min, max})); } -common_peg_parser common_peg_parser_builder::schema(const common_peg_parser & p, const std::string & name, const nlohmann::ordered_json & schema, bool raw) { - return wrap(arena_.add_parser(common_peg_schema_parser{p.id(), name, std::make_shared(schema), raw})); +common_peg_parser common_peg_parser_builder::schema(const common_peg_parser & p, const std::string & name, const common_json & schema, bool raw) { + return wrap(arena_.add_parser(common_peg_schema_parser{p.id(), name, std::make_shared(schema), raw})); } common_peg_parser common_peg_parser_builder::rule(const std::string & name, const common_peg_parser & p, bool trigger) { @@ -1805,8 +1804,8 @@ void common_peg_arena::build_grammar(const common_grammar_builder & builder, boo } } -static nlohmann::json serialize_parser_variant(const common_peg_parser_variant & variant) { - using json = nlohmann::json; +static common_json serialize_parser_variant(const common_peg_parser_variant & variant) { + using json = common_json; return std::visit([](const auto & p) -> json { using T = std::decay_t; @@ -1860,7 +1859,7 @@ static nlohmann::json serialize_parser_variant(const common_peg_parser_variant & {"type", "schema"}, {"child", p.child}, {"name", p.name}, - {"schema", p.schema ? *p.schema : nullptr}, + {"schema", p.schema ? *p.schema : json(nullptr)}, {"raw", p.raw} }; } else if constexpr (std::is_same_v) { @@ -1888,19 +1887,19 @@ static nlohmann::json serialize_parser_variant(const common_peg_parser_variant & }, variant); } -nlohmann::json common_peg_arena::to_json() const { - auto parsers = nlohmann::json::array(); +common_json common_peg_arena::to_json() const { + auto parsers = common_json::array(); for (const auto & parser : parsers_) { parsers.push_back(serialize_parser_variant(parser)); } - return nlohmann::json{ + return common_json{ {"parsers", parsers}, {"rules", rules_}, {"root", root_} }; } -static common_peg_parser_variant deserialize_parser_variant(const nlohmann::json & j) { +static common_peg_parser_variant deserialize_parser_variant(const common_json & j) { if (!j.contains("type") || !j["type"].is_string()) { throw std::runtime_error("Parser variant JSON missing or invalid 'type' field"); } @@ -1969,9 +1968,9 @@ static common_peg_parser_variant deserialize_parser_variant(const nlohmann::json } common_peg_chars_parser parser; parser.pattern = j["pattern"]; - parser.negated = j["negated"]; - parser.min_count = j["min_count"]; - parser.max_count = j["max_count"]; + parser.negated = j["negated"].get(); + parser.min_count = j["min_count"].get(); + parser.max_count = j["max_count"].get(); for (const auto & range_json : j["ranges"]) { if (!range_json.contains("start") || !range_json.contains("end")) { throw std::runtime_error("char_range missing 'start' or 'end' field"); @@ -2007,7 +2006,7 @@ static common_peg_parser_variant deserialize_parser_variant(const nlohmann::json parser.child = j["child"].get(); parser.name = j["name"]; if (!j["schema"].is_null()) { - parser.schema = std::make_shared(j["schema"]); + parser.schema = std::make_shared(j["schema"]); } parser.raw = j["raw"].get(); return parser; @@ -2069,7 +2068,7 @@ static common_peg_parser_variant deserialize_parser_variant(const nlohmann::json throw std::runtime_error("Unknown parser type: " + type); } -common_peg_arena common_peg_arena::from_json(const nlohmann::json & j) { +common_peg_arena common_peg_arena::from_json(const common_json & j) { if (!j.contains("parsers") || !j["parsers"].is_array()) { throw std::runtime_error("JSON missing or invalid 'parsers' array"); } @@ -2109,7 +2108,7 @@ std::string common_peg_arena::save() const { } void common_peg_arena::load(const std::string & data) { - *this = from_json(nlohmann::json::parse(data)); + *this = from_json(common_json::parse(data)); } common_peg_arena build_peg_parser(const std::function & fn) { diff --git a/common/peg-parser.h b/common/peg-parser.h index c198499dd..ab095cc7d 100644 --- a/common/peg-parser.h +++ b/common/peg-parser.h @@ -1,6 +1,6 @@ #pragma once -#include +#include "json.h" #include #include @@ -245,7 +245,7 @@ struct common_peg_until_parser { struct common_peg_schema_parser { common_peg_parser_id child; std::string name; - std::shared_ptr schema; + std::shared_ptr schema; // Indicates if the GBNF should accept a raw string that matches the schema. bool raw; @@ -332,8 +332,8 @@ class common_peg_arena { std::string dump(common_peg_parser_id id) const; - nlohmann::json to_json() const; - static common_peg_arena from_json(const nlohmann::json & j); + common_json to_json() const; + static common_peg_arena from_json(const common_json & j); std::string save() const; void load(const std::string & data); @@ -490,7 +490,7 @@ class common_peg_parser_builder { // Wraps a parser with JSON schema metadata for grammar generation. // Used internally to convert JSON schemas to GBNF grammar rules. - common_peg_parser schema(const common_peg_parser & p, const std::string & name, const nlohmann::ordered_json & schema, bool raw = false); + common_peg_parser schema(const common_peg_parser & p, const std::string & name, const common_json & schema, bool raw = false); // Creates a named rule, stores it in the grammar, and returns a ref. // If trigger=true, marks this rule as an entry point for lazy grammar generation. diff --git a/tests/peg-parser/test-json-serialization.cpp b/tests/peg-parser/test-json-serialization.cpp index a85801060..da63a23bf 100644 --- a/tests/peg-parser/test-json-serialization.cpp +++ b/tests/peg-parser/test-json-serialization.cpp @@ -8,7 +8,7 @@ void test_json_serialization(testing &t) { auto json_serialized = original.to_json().dump(); t.test("compare before/after", [&](testing &t) { - auto deserialized = common_peg_arena::from_json(nlohmann::json::parse(json_serialized)); + auto deserialized = common_peg_arena::from_json(common_json::parse(json_serialized)); // Test complex JSON std::string input = R"({"name": "test", "values": [1, 2, 3], "nested": {"a": true}})"; @@ -23,6 +23,6 @@ void test_json_serialization(testing &t) { }); t.bench("deserialize", [&]() { - auto deserialized = common_peg_arena::from_json(nlohmann::json::parse(json_serialized)); + auto deserialized = common_peg_arena::from_json(common_json::parse(json_serialized)); }, 100); } diff --git a/tests/peg-parser/tests.h b/tests/peg-parser/tests.h index debd4286c..00e81815b 100644 --- a/tests/peg-parser/tests.h +++ b/tests/peg-parser/tests.h @@ -1,7 +1,7 @@ #pragma once // Common includes for all test files -#include +#include "json.h" #include #include @@ -11,9 +11,9 @@ #include "simple-tokenize.h" struct bench_tool_call { - std::string id; - std::string name; - nlohmann::ordered_json args; + std::string id; + std::string name; + common_json args; }; // Test function declarations diff --git a/tests/test-chat-peg-parser.cpp b/tests/test-chat-peg-parser.cpp index 3ab7a67b6..793891394 100644 --- a/tests/test-chat-peg-parser.cpp +++ b/tests/test-chat-peg-parser.cpp @@ -11,9 +11,9 @@ #include #include -#include "nlohmann/json.hpp" +#include "json.h" -using json = nlohmann::ordered_json; +using json = common_json; static json create_tools(); static void test_example_native(testing & t); @@ -63,10 +63,10 @@ static json create_tools() { { { "type", "string" }, { "description", "The city and state, e.g. San Francisco, CA" } } }, { "unit", { { "type", "string" }, - { "enum", { "celsius", "fahrenheit" } }, + { "enum", json::array({ "celsius", "fahrenheit" }) }, { "description", "The temperature unit to use. Infer this from the users location." } } } } }, - { "required", { "location", "unit" } }, + { "required", json::array({ "location", "unit" }) }, } }, } } }; @@ -86,14 +86,14 @@ static json create_tools() { { { "type", "string" }, { "description", "The city and state, e.g. San Francisco, CA" } } }, { "unit", { { "type", "string" }, - { "enum", { "celsius", "fahrenheit" } }, + { "enum", json::array({ "celsius", "fahrenheit" }) }, { "description", "The temperature unit to use. Infer this from the users location." } } }, { "days", { { "type", "integer" }, { "description", "Number of days to forecast (1-10)" }, { "minimum", 1 }, { "maximum", 10 } } } } }, - { "required", { "location", "unit" } }, + { "required", json::array({ "location", "unit" }) }, } }, } } }; @@ -114,9 +114,9 @@ static json create_tools() { { "default", 5 } } }, { "category", { { "type", "string" }, - { "enum", { "api", "troubleshooting", "billing", "general" } }, + { "enum", json::array({ "api", "troubleshooting", "billing", "general" }) }, { "description", "Filter search by specific category." } } } } }, - { "required", { "query", "category" } }, + { "required", json::array({ "query", "category" }) }, { "additionalProperties", false } } }, { "strict", true } } } }; @@ -341,7 +341,7 @@ static void test_example_native(testing & t) { { { "invoice_number", { { "type", "string" } } }, { "amount", { { "type", "number" } } }, { "due_date", { { "type", "string" } } } } }, - { "required", { "invoice_number", "amount", "due_date" } } }, + { "required", json::array({ "invoice_number", "amount", "due_date" }) } }, /* .parallel_tool_calls = */ false, /* .generation_prompt = */ "", /* .input = */ @@ -406,7 +406,7 @@ static void test_example_qwen3_coder(testing & t) { std::set required_properties; if (function.contains("required")) { - function.at("required").get_to(required_properties); + required_properties = function.at("required").get>(); } std::vector arg_parsers; @@ -661,8 +661,8 @@ void test_command7_parser_compare(testing & t) { "5. Provide a detailed cost breakdown that includes accommodation, transportation, meals, and entry fees " "to attractions."; - std::vector> tool_calls = { - { "call_0", "plan_trip", nlohmann::json::parse(R"({ + std::vector> tool_calls = { + { "call_0", "plan_trip", common_json::parse(R"({ "destination": "Japan", "duration": 14, "budget": 4000, @@ -686,16 +686,16 @@ void test_command7_parser_compare(testing & t) { if (!tool_calls.empty()) { tokens.emplace_back("<|START_ACTION|>"); - auto json = nlohmann::json::array(); + auto json = common_json::array(); for (const auto & tc : tool_calls) { - auto tc_json = nlohmann::json::object(); + auto tc_json = common_json::object(); tc_json["tool_call_id"] = std::get<0>(tc); tc_json["tool_name"] = std::get<1>(tc); tc_json["parameters"] = std::get<2>(tc); json.push_back(tc_json); } - auto tokenized = simple_tokenize(json.dump(-1, ' ', true)); + auto tokenized = simple_tokenize(json.dump(-1)); tokens.insert(tokens.end(), tokenized.begin(), tokenized.end()); tokens.emplace_back("<|END_ACTION|>"); @@ -737,7 +737,7 @@ static void test_prefix_tool_names(testing & t) { { { "arg1", { { "type", "integer" } } }, } }, - { "required", { "arg1" } }, + { "required", json::array({ "arg1" }) }, } }, } } }; @@ -757,7 +757,7 @@ static void test_prefix_tool_names(testing & t) { { "arg1", { { "type", "integer" } } }, { "arg2", { { "type", "integer" } } }, } }, - { "required", { "arg1" } }, + { "required", json::array({ "arg1" }) }, } }, } } }; diff --git a/tests/test-chat-template.cpp b/tests/test-chat-template.cpp index 6a6292cd0..bcc574afe 100644 --- a/tests/test-chat-template.cpp +++ b/tests/test-chat-template.cpp @@ -7,7 +7,7 @@ #include #include -#include +#include "json.h" #undef NDEBUG #include @@ -20,7 +20,7 @@ #include "jinja/lexer.h" #include "jinja/caps.h" -using json = nlohmann::ordered_json; +using json = common_json; static int main_automated_tests(void); @@ -304,8 +304,8 @@ void run_single(const std::string& contents, json input, bool use_common, bool d if (input.contains("eos_token")) { eos_token = input["eos_token"].get(); } - nlohmann::ordered_json msgs_json = input["messages"]; - nlohmann::ordered_json tools_json = input["tools"]; + common_json msgs_json = input["messages"]; + common_json tools_json = input["tools"]; auto messages = common_chat_msgs_parse_oaicompat(msgs_json); auto tools = common_chat_tools_parse_oaicompat(tools_json); auto output = format_using_common(contents, bos_token, eos_token, messages, tools); diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp index c4670da85..7918f0ffc 100644 --- a/tests/test-chat.cpp +++ b/tests/test-chat.cpp @@ -19,12 +19,12 @@ #include #include #include -#include +#include "json.h" #include #include #include -using json = nlohmann::ordered_json; +using json = common_json; static std::ostream & operator<<(std::ostream & os, const common_chat_msg_diff & diff) { os << "{ content_delta: " << diff.content_delta << "; "; diff --git a/tests/test-grammar-integration.cpp b/tests/test-grammar-integration.cpp index 4d5d13dd0..eb4b7c78f 100644 --- a/tests/test-grammar-integration.cpp +++ b/tests/test-grammar-integration.cpp @@ -7,13 +7,13 @@ #include "../src/unicode.h" #include "../src/llama-grammar.h" -#include +#include "json.h" #include #include #include -using json = nlohmann::ordered_json; +using json = common_json; static llama_grammar * build_grammar_with_root(const std::string & grammar_str, const char * grammar_root) { return llama_grammar_init_impl(nullptr, grammar_str.c_str(), grammar_root, false, nullptr, 0, nullptr, 0); diff --git a/tests/test-jinja.cpp b/tests/test-jinja.cpp index 1eb2a062b..974a3f9dd 100644 --- a/tests/test-jinja.cpp +++ b/tests/test-jinja.cpp @@ -3,7 +3,7 @@ #include #include -#include +#include "json.h" #include "subproc.h" #include "jinja/runtime.h" @@ -14,7 +14,7 @@ #include "testing.h" -using json = nlohmann::ordered_json; +using json = common_json; static void test_template(testing & t, const std::string & name, const std::string & tmpl, const json & vars, const std::string & expect); @@ -240,7 +240,7 @@ static void test_conditionals(testing & t) { test_template(t, "is undefined key falsy", "{{ 'yes' if not y['x'] else 'no' }}", - {{"y", {{}}}}, + {{"y", json::array({nullptr})}}, "yes" ); @@ -282,7 +282,7 @@ static void test_conditionals(testing & t) { test_template(t, "is non-empty object truthy", "{{ 'yes' if y else 'no' }}", - {{"y", {"x", false}}}, + {{"y", json::array({"x", false})}}, "yes" ); diff --git a/tests/test-json-schema-to-grammar.cpp b/tests/test-json-schema-to-grammar.cpp index 74b57cf1b..214dbe199 100755 --- a/tests/test-json-schema-to-grammar.cpp +++ b/tests/test-json-schema-to-grammar.cpp @@ -6,7 +6,7 @@ #include "../src/llama-grammar.h" -#include +#include "json.h" #include #include @@ -1442,7 +1442,7 @@ static void test_resolves_to_string() { auto test = [](const std::string & name, const std::string & schema_str, bool expected) { fprintf(stderr, "- %s\n", name.c_str()); common_schema_info info; - auto schema = nlohmann::ordered_json::parse(schema_str); + auto schema = common_json::parse(schema_str); info.resolve_refs(schema); bool result = info.resolves_to_string(schema); if (result != expected) { @@ -1517,7 +1517,7 @@ int main() { test_all("C++", [](const TestCase & tc) { try { - tc.verify(json_schema_to_grammar(nlohmann::ordered_json::parse(tc.schema), true)); + tc.verify(json_schema_to_grammar(common_json::parse(tc.schema), true)); tc.verify_status(SUCCESS); } catch (const std::invalid_argument & ex) { fprintf(stderr, "Error: %s\n", ex.what()); @@ -1531,7 +1531,7 @@ int main() { auto run = [](const TestCase & tc) { fprintf(stderr, "- %s\n", tc.name.c_str()); try { - tc.verify(json_schema_to_grammar(nlohmann::ordered_json::parse(tc.schema), true)); + tc.verify(json_schema_to_grammar(common_json::parse(tc.schema), true)); tc.verify_status(SUCCESS); } catch (const std::invalid_argument & ex) { fprintf(stderr, "Error: %s\n", ex.what()); diff --git a/tests/test-model-resolution.cpp b/tests/test-model-resolution.cpp index 2437eeec6..5191e7751 100644 --- a/tests/test-model-resolution.cpp +++ b/tests/test-model-resolution.cpp @@ -9,7 +9,7 @@ #include "http.h" #include "log.h" -#include +#include "json.h" #include #include @@ -55,7 +55,7 @@ static const char * COMMIT = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; static void serve_repos(httplib::Server & server) { server.Get(R"(/api/models/(.+)/refs)", [](const httplib::Request & req, httplib::Response & res) { if (g_repos.count(req.matches[1])) { - res.set_content(nlohmann::json{{"branches", {{{"name", "main"}, {"targetCommit", COMMIT}}}}}.dump(), + res.set_content(common_json{{"branches", common_json::array({ common_json{{"name", "main"}, {"targetCommit", COMMIT}} })}}.dump(), "application/json"); } else { res.status = 404; @@ -66,7 +66,7 @@ static void serve_repos(httplib::Server & server) { res.status = 404; return; } - auto files = nlohmann::json::array(); + auto files = common_json::array(); size_t i = 0; for (const auto & p : g_repos[req.matches[1]]) { char oid[41]; diff --git a/tools/cli/cli-context.cpp b/tools/cli/cli-context.cpp index 3d801b73d..aa4eb7679 100644 --- a/tools/cli/cli-context.cpp +++ b/tools/cli/cli-context.cpp @@ -6,8 +6,7 @@ #include "log.h" #include "console.h" -#define JSON_ASSERT GGML_ASSERT -#include +#include "json.h" #include #include @@ -16,7 +15,7 @@ #include #include -using json = nlohmann::ordered_json; +using json = common_json; struct cli_context_impl { json messages = json::array(); @@ -73,7 +72,7 @@ static std::string format_error_message(const json & err) { // err is the raw response body of a failed request; it may or may not be JSON static std::string format_error_message(const std::string & err) { - json parsed = json::parse(err, nullptr, false); + json parsed = json::parse_no_throw(err); if (!parsed.is_discarded()) { return format_error_message(parsed); } @@ -157,7 +156,7 @@ bool cli_context::init() { if (!list_and_ask_models()) { return false; } - } catch (const json::parse_error & e) { + } catch (const common_json_error & e) { ui::show_error(e.what()); ui::show_message("This might be caused by an incorrect server-base endpoint URL"); return false; @@ -364,7 +363,7 @@ bool cli_context::generate_completion(generated_content & content_out, cli_timin ui::assistant_turn a; std::string err = client.post_sse("/v1/chat/completions", body.dump(), should_stop, [&](const std::string & payload) { - json chunk = json::parse(payload, nullptr, false); + json chunk = json::parse_no_throw(payload); if (chunk.is_discarded()) { return; } diff --git a/tools/parser/debug-template-parser.cpp b/tools/parser/debug-template-parser.cpp index 8a916f79c..abe427022 100644 --- a/tools/parser/debug-template-parser.cpp +++ b/tools/parser/debug-template-parser.cpp @@ -5,7 +5,7 @@ #include "gguf.h" #include "jinja/runtime.h" #include "log.h" -#include "nlohmann/json.hpp" +#include "json.h" #include "peg-parser.h" #include @@ -15,7 +15,7 @@ #include #include -using json = nlohmann::ordered_json; +using json = common_json; enum class output_mode { ANALYSIS, // Only output analysis results (default) diff --git a/tools/parser/template-analysis.cpp b/tools/parser/template-analysis.cpp index bf898a229..11225bd8c 100644 --- a/tools/parser/template-analysis.cpp +++ b/tools/parser/template-analysis.cpp @@ -11,9 +11,9 @@ #include #include -#include "nlohmann/json.hpp" +#include "json.h" -using json = nlohmann::ordered_json; +using json = common_json; // ANSI color codes - using 256-color palette for brighter colors (all bold) #define ANSI_RESET "\033[0m" diff --git a/tools/server/server-chat.cpp b/tools/server/server-chat.cpp index 0322e54cc..a6fe3c6ba 100644 --- a/tools/server/server-chat.cpp +++ b/tools/server/server-chat.cpp @@ -153,7 +153,7 @@ json server_chat_convert_responses_to_chatcmpl(const json & response_body) { prev_msg["content"] = json::array(); } auto & prev_content = prev_msg["content"]; - prev_content.insert(prev_content.end(), chatcmpl_content.begin(), chatcmpl_content.end()); + prev_content.insert(chatcmpl_content); } else { item.erase("status"); item.erase("type"); diff --git a/tools/server/server-chat.h b/tools/server/server-chat.h index 102eae688..86b842650 100644 --- a/tools/server/server-chat.h +++ b/tools/server/server-chat.h @@ -6,9 +6,7 @@ #include "server-common.h" #include "server-http.h" -#include - -using json = nlohmann::ordered_json; +#include "json.h" // Convert OpenAI Responses API format to OpenAI Chat Completions API format json server_chat_convert_responses_to_chatcmpl(const json & body); diff --git a/tools/server/server-common.cpp b/tools/server/server-common.cpp index 585f65e83..4f5b8202a 100644 --- a/tools/server/server-common.cpp +++ b/tools/server/server-common.cpp @@ -1540,7 +1540,7 @@ std::vector get_token_probabilities(llama_context * ctx, int i } std::string safe_json_to_str(const json & data) { - return data.dump(-1, ' ', false, json::error_handler_t::replace); + return data.dump_safe(); } // TODO: reuse llama_detokenize diff --git a/tools/server/server-common.h b/tools/server/server-common.h index 6488be344..f8ea82ef4 100644 --- a/tools/server/server-common.h +++ b/tools/server/server-common.h @@ -6,8 +6,7 @@ #include "chat.h" #include "mtmd.h" -#define JSON_ASSERT GGML_ASSERT -#include +#include "json.h" #include #include @@ -19,7 +18,7 @@ #include #include -using json = nlohmann::ordered_json; +using json = common_json; #define SLT_DBG(slot, fmt, ...) LOG_DBG("slot %12.*s: id %2d | task %d | " fmt, 12, __func__, (slot).id, ((slot).task ? (slot).task->id : -1), __VA_ARGS__) #define SLT_TRC(slot, fmt, ...) LOG_TRC("slot %12.*s: id %2d | task %d | " fmt, 12, __func__, (slot).id, ((slot).task ? (slot).task->id : -1), __VA_ARGS__) @@ -42,9 +41,9 @@ static T json_value(const json & body, const std::string & key, const T & defaul // Fallback null to default value if (body.contains(key) && !body.at(key).is_null()) { try { - return body.at(key); - } catch (NLOHMANN_JSON_NAMESPACE::detail::type_error const & err) { - LOG_WRN("Wrong type supplied for parameter '%s'. Expected '%s', using default value: %s\n", key.c_str(), json(default_value).type_name(), err.what()); + return body.at(key).get(); + } catch (const common_json_error & err) { + LOG_WRN("Wrong type supplied for parameter '%s', using default value: %s\n", key.c_str(), err.what()); return default_value; } } else { diff --git a/tools/server/server-context.cpp b/tools/server/server-context.cpp index 36d982832..572682af7 100644 --- a/tools/server/server-context.cpp +++ b/tools/server/server-context.cpp @@ -35,8 +35,6 @@ #include #endif -using json = nlohmann::ordered_json; - constexpr int HTTP_POLLING_SECONDS = 1; static common_speculative_output_limits server_output_limits(const common_params & params) { @@ -657,14 +655,14 @@ struct server_slot { res["n_prompt_tokens_processed"] = stats.n_prompt_processed; res["n_prompt_tokens_cache"] = stats.n_prompt_cached; res["params"] = ptask->params.to_json(only_metrics); - res["next_token"] = { + res["next_token"] = json::array({ { {"has_next_token", has_next_token}, {"has_new_line", has_new_line}, {"n_remain", n_remaining()}, {"n_decoded", stats.n_gen}, } - }; + }); if (!only_metrics) { res["prompt"] = ptask->tokens.detokenize(ctx_tgt, true); @@ -4165,7 +4163,8 @@ std::unique_ptr server_routes::handle_completions_impl( // tasks.reserve(inputs.size()); // TODO: this is inaccurate due to child tasks // message delimiters for checkpointing - auto delimiters = common_chat_msg_delimiters_parse(json_value(data, "message_delimiters", json::array())); + json delims = json_value(data, "message_delimiters", json::array()); + auto delimiters = common_chat_msg_delimiters_parse(delims); delimiters.tokenize(ctx_server.vocab); for (size_t i = 0; i < inputs.size(); i++) { @@ -4428,8 +4427,8 @@ static json get_res_model_info(const server_context_meta & meta) { static json get_res_models(const server_context_meta & meta) { // note: do NOT use ctx_server here, otherwise it's not possible to use this during sleep - return { - {"models", { + return json{ + {"models", json::array({ { {"name", meta.model_name}, {"model", meta.model_name}, @@ -4438,23 +4437,23 @@ static json get_res_models(const server_context_meta & meta) { {"digest", ""}, // dummy value, llama.cpp does not support managing model file's hash {"type", "model"}, {"description", ""}, - {"tags", {""}}, - {"capabilities", meta.has_mtmd ? json({"completion","multimodal"}) : json({"completion"})}, + {"tags", json::array({""})}, + {"capabilities", meta.has_mtmd ? json::array({"completion","multimodal"}) : json::array({"completion"})}, {"parameters", ""}, {"details", { {"parent_model", ""}, {"format", "gguf"}, {"family", ""}, - {"families", {""}}, + {"families", json::array({""})}, {"parameter_size", ""}, {"quantization_level", ""} }} } - }}, + })}, {"object", "list"}, - {"data", { + {"data", json::array({ get_res_model_info(meta), - }} + })} }; } @@ -4990,7 +4989,7 @@ void server_routes::init_routes() { std::string content; if (body.count("tokens") != 0) { - const llama_tokens tokens = body.at("tokens"); + const llama_tokens tokens = body.at("tokens").get(); content = tokens_to_str(ctx_server.vocab, tokens); } @@ -5297,7 +5296,7 @@ std::unique_ptr server_routes::handle_embeddings_impl(cons int embd_normalize = params.embd_normalize; if (body.count("embd_normalize") != 0) { - embd_normalize = body.at("embd_normalize"); + embd_normalize = body.at("embd_normalize").get(); if (meta->pooling_type == LLAMA_POOLING_TYPE_NONE) { SRV_DBG("embd_normalize is not supported by pooling type %d, ignoring it\n", meta->pooling_type); } diff --git a/tools/server/server-context.h b/tools/server/server-context.h index 764df0e08..5d464b8e8 100644 --- a/tools/server/server-context.h +++ b/tools/server/server-context.h @@ -4,7 +4,7 @@ #include "server-task.h" #include "server-queue.h" -#include +#include "json.h" #include #include diff --git a/tools/server/server-models.cpp b/tools/server/server-models.cpp index d60545194..db0fac995 100644 --- a/tools/server/server-models.cpp +++ b/tools/server/server-models.cpp @@ -2462,7 +2462,7 @@ server_http_proxy::server_http_proxy( bool has_files = !files.empty(); if (has_files) { - json form_fields = json::parse(body, nullptr, false); + json form_fields = json::parse_no_throw(body); if (!form_fields.is_discarded()) { auto boundary = generate_multipart_boundary(); effective_body = build_multipart_body(form_fields, files, boundary); diff --git a/tools/server/server-schema.cpp b/tools/server/server-schema.cpp index 5d7fa6ae6..64b925129 100644 --- a/tools/server/server-schema.cpp +++ b/tools/server/server-schema.cpp @@ -503,7 +503,7 @@ std::vector> make_llama_cmpl_schema(const common_params & ->set_handler([&](field_eval_context & ctx, const json & data) { const auto & samplers = data.at("samplers"); if (samplers.is_array()) { - ctx.params.sampling.samplers = common_sampler_types_from_names(samplers); + ctx.params.sampling.samplers = common_sampler_types_from_names(samplers.get>()); } else if (samplers.is_string()) { ctx.params.sampling.samplers = common_sampler_types_from_chars(samplers.get()); } @@ -580,8 +580,7 @@ static void handle_with_catch(const char * name, std::function func) { // treat a null value as absent so clients can send null to request the server default static bool has_value(const json & data, const char * n) { - auto it = data.find(n); - return it != data.end() && !it->is_null(); + return data.contains(n) && !data.at(n).is_null(); } template diff --git a/tools/server/server-task.cpp b/tools/server/server-task.cpp index 258cdcf8f..0d3beb313 100644 --- a/tools/server/server-task.cpp +++ b/tools/server/server-task.cpp @@ -12,8 +12,6 @@ #include -using json = nlohmann::ordered_json; - // // task_params // @@ -304,7 +302,7 @@ json completion_token_output::probs_vector_to_json(const std::vector::lowest() : std::log(x); } @@ -407,7 +405,7 @@ json server_task_result_cmpl_final::to_json_oaicompat() { res["__verbose"] = to_json_non_oaicompat(); } if (stats.is_set()) { - res.push_back({"timings", stats.to_json()}); + res["timings"] = stats.to_json(); } return res; @@ -455,7 +453,7 @@ json server_task_result_cmpl_final::to_json_oaicompat_chat() { res["__verbose"] = to_json_non_oaicompat(); } if (stats.is_set()) { - res.push_back({"timings", stats.to_json()}); + res["timings"] = stats.to_json(); } return res; @@ -516,7 +514,7 @@ json server_task_result_cmpl_final::to_json_oaicompat_chat_stream() { } if (stats.is_set()) { - deltas.back().push_back({"timings", stats.to_json()}); + deltas.back()["timings"] = stats.to_json(); } // extra fields for debugging purposes @@ -709,7 +707,7 @@ json server_task_result_cmpl_final::to_json_oaicompat_resp_stream() { }); if (stats.is_set()) { - server_sent_events.back().at("data").push_back({"timings", stats.to_json()}); + server_sent_events.back().at("data")["timings"] = stats.to_json(); } return server_sent_events; @@ -1061,10 +1059,10 @@ json server_task_result_cmpl_partial::to_json_non_oaicompat() { }; // populate the timings object when needed (usually for the last response or with timings_per_token enabled) if (stats.is_set()) { - res.push_back({"timings", stats.to_json()}); + res["timings"] = stats.to_json(); } if (is_progress) { - res.push_back({"prompt_progress", progress.to_json()}); + res["prompt_progress"] = progress.to_json(); } if (!prob_output.probs.empty()) { res["completion_probabilities"] = completion_token_output::probs_vector_to_json({prob_output}, post_sampling_probs); @@ -1101,10 +1099,10 @@ json server_task_result_cmpl_partial::to_json_oaicompat() { res["__verbose"] = to_json_non_oaicompat(); } if (stats.is_set()) { - res.push_back({"timings", stats.to_json()}); + res["timings"] = stats.to_json(); } if (is_progress) { - res.push_back({"prompt_progress", progress.to_json()}); + res["prompt_progress"] = progress.to_json(); } return res; @@ -1155,10 +1153,10 @@ json server_task_result_cmpl_partial::to_json_oaicompat_chat() { } if (stats.is_set()) { - last_json.push_back({"timings", stats.to_json()}); + last_json["timings"] = stats.to_json(); } if (is_progress) { - last_json.push_back({"prompt_progress", progress.to_json()}); + last_json["prompt_progress"] = progress.to_json(); } } @@ -1305,10 +1303,10 @@ json server_task_result_cmpl_partial::to_json_oaicompat_resp() { if (!events.empty()) { json & data = events.back().at("data"); if (stats.is_set()) { - data.push_back({"timings", stats.to_json()}); + data["timings"] = stats.to_json(); } if (is_progress) { - data.push_back({"prompt_progress", progress.to_json()}); + data["prompt_progress"] = progress.to_json(); } } diff --git a/tools/server/server-task.h b/tools/server/server-task.h index 25ff01512..9c99143f8 100644 --- a/tools/server/server-task.h +++ b/tools/server/server-task.h @@ -11,7 +11,6 @@ // TODO: prevent including the whole server-common.h as we only use server_tokens #include "server-common.h" -using json = nlohmann::ordered_json; enum server_task_type { SERVER_TASK_TYPE_COMPLETION, diff --git a/tools/server/server-tools.cpp b/tools/server/server-tools.cpp index b5c5c078a..12e9dbb8c 100644 --- a/tools/server/server-tools.cpp +++ b/tools/server/server-tools.cpp @@ -2156,7 +2156,7 @@ void server_tools::setup(const std::vector & enabled_tools, res->status = 200; res->data = safe_json_to_str(result); } - } catch (const json::exception & e) { + } catch (const common_json_error & e) { res->status = 400; res->data = safe_json_to_str(format_error_response(e.what(), ERROR_TYPE_INVALID_REQUEST)); } catch (const std::invalid_argument & e) { From b21e4de74567f5eef213765c9476a843c2e43f0d Mon Sep 17 00:00:00 2001 From: Xuan-Son Nguyen Date: Sat, 22 Aug 2026 16:33:47 +0200 Subject: [PATCH 02/28] mtmd: use ggml_rope_set_offset (#27521) * mtmd: use ggml_rope_set_offset * add comment --- tools/mtmd/clip-graph.h | 12 ++++-- tools/mtmd/clip.cpp | 66 +++++++++++--------------------- tools/mtmd/models/gemma4v.cpp | 64 +++++++++++-------------------- tools/mtmd/models/minimax-m3.cpp | 24 ++++-------- 4 files changed, 62 insertions(+), 104 deletions(-) diff --git a/tools/mtmd/clip-graph.h b/tools/mtmd/clip-graph.h index 2cf1b683a..bbee35bea 100644 --- a/tools/mtmd/clip-graph.h +++ b/tools/mtmd/clip-graph.h @@ -137,9 +137,15 @@ struct clip_graph { int il, ggml_tensor * sinks = nullptr) const; - // implementation of the 2D RoPE without adding a new op in ggml - // this is not efficient (use double the memory), but works on all backends - // TODO: there was a more efficient which relies on ggml_view and ggml_rope_ext_inplace, but the rope inplace does not work well with non-contiguous tensors ; we should fix that and revert back to the original implementation in https://github.com/ggml-org/llama.cpp/pull/13065 + // implementation of the 2D RoPE using two ggml_rope_ext calls + // + // unlike GGML_ROPE_TYPE_VISION which forces NEOX ordering, this rotates adjacent pairs (normal ordering) + // + // example: + // given a single head with size = 8 --> [00000000] + // dims [0, 4) rotate with pos_a, dims [4, 8) rotate with pos_b --> [aaaabbbb] + // interleave_freq = false --> both halves use the same inv_freq set (like GGML_ROPE_TYPE_VISION) + // interleave_freq = true --> first half uses even inv_freq, second half uses odd inv_freq (used by pixtral) ggml_tensor * build_rope_2d( ggml_context * ctx0, ggml_tensor * cur, diff --git a/tools/mtmd/clip.cpp b/tools/mtmd/clip.cpp index 9977ed490..89ca65a7b 100644 --- a/tools/mtmd/clip.cpp +++ b/tools/mtmd/clip.cpp @@ -819,8 +819,6 @@ ggml_tensor * clip_graph::build_attn( } // implementation of the 2D RoPE without adding a new op in ggml -// this is not efficient (use double the memory), but works on all backends -// TODO: there was a more efficient which relies on ggml_view and ggml_rope_ext_inplace, but the rope inplace does not work well with non-contiguous tensors ; we should fix that and revert back to the original implementation in https://github.com/ggml-org/llama.cpp/pull/13065 ggml_tensor * clip_graph::build_rope_2d( ggml_context * ctx0, ggml_tensor * cur, @@ -829,9 +827,7 @@ ggml_tensor * clip_graph::build_rope_2d( const float freq_base, const bool interleave_freq ) { - const int64_t n_dim = cur->ne[0]; - const int64_t n_head = cur->ne[1]; - const int64_t n_pos = cur->ne[2]; + const int64_t n_dim = cur->ne[0]; // for example, if we have cur tensor of shape (n_dim=8, n_head, n_pos) // we will have a list of 4 inv_freq: 1e-0, 1e-1, 1e-2, 1e-3 @@ -845,46 +841,30 @@ ggml_tensor * clip_graph::build_rope_2d( ? std::pow(freq_base, (float)-2/n_dim) : 1.0; - // first half - ggml_tensor * first; - { - first = ggml_view_3d(ctx0, cur, - n_dim/2, n_head, n_pos, - cur->nb[1], - cur->nb[2], - 0); - first = ggml_rope_ext( - ctx0, - first, - pos_a, // positions - nullptr, // freq factors - n_dim/2, // n_dims - 0, 0, freq_base, - 1.0f, 0.0f, 1.0f, 0.0f, 0.0f - ); - } + // first half, dims [0, n_dim/2) + cur = ggml_rope_ext( + ctx0, + cur, + pos_a, // positions + nullptr, // freq factors + n_dim/2, // n_dims + 0, 0, freq_base, + 1.0f, 0.0f, 1.0f, 0.0f, 0.0f + ); - // second half - ggml_tensor * second; - { - second = ggml_view_3d(ctx0, cur, - n_dim/2, n_head, n_pos, - cur->nb[1], - cur->nb[2], - n_dim/2 * ggml_element_size(cur)); - second = ggml_rope_ext( - ctx0, - second, - pos_b, // positions - nullptr, // freq factors - n_dim/2, // n_dims - 0, 0, freq_base, - freq_scale_odd, - 0.0f, 1.0f, 0.0f, 0.0f - ); - } + // second half, dims [n_dim/2, n_dim) + cur = ggml_rope_ext( + ctx0, + cur, + pos_b, // positions + nullptr, // freq factors + n_dim/2, // n_dims + 0, 0, freq_base, + freq_scale_odd, + 0.0f, 1.0f, 0.0f, 0.0f + ); + cur = ggml_rope_set_offset(cur, n_dim/2); - cur = ggml_concat(ctx0, first, second, 0); return cur; } diff --git a/tools/mtmd/models/gemma4v.cpp b/tools/mtmd/models/gemma4v.cpp index 87cbd43fc..448438947 100644 --- a/tools/mtmd/models/gemma4v.cpp +++ b/tools/mtmd/models/gemma4v.cpp @@ -44,51 +44,31 @@ ggml_cgraph * clip_graph_gemma4v::build() { // similar to build_rope_2d, but use neox ordering auto add_pos = [&](ggml_tensor * cur, const clip_layer &) { - const int64_t n_dim = cur->ne[0]; - const int64_t n_head = cur->ne[1]; - const int64_t n_pos = cur->ne[2]; + const int64_t n_dim = cur->ne[0]; - // first half - ggml_tensor * first; - { - first = ggml_view_4d(ctx0, cur, - n_dim/2, n_head, n_pos, n_batch, - cur->nb[1], - cur->nb[2], - cur->nb[3], - 0); - first = ggml_rope_ext( - ctx0, - first, - pos_x, // positions - nullptr, // freq factors - n_dim/2, // n_dims - GGML_ROPE_TYPE_NEOX, 0, hparams.rope_theta, - 1.0f, 0.0f, 1.0f, 0.0f, 0.0f - ); - } + // first half, dims [0, n_dim/2) + cur = ggml_rope_ext( + ctx0, + cur, + pos_x, // positions + nullptr, // freq factors + n_dim/2, // n_dims + GGML_ROPE_TYPE_NEOX, 0, hparams.rope_theta, + 1.0f, 0.0f, 1.0f, 0.0f, 0.0f + ); - // second half - ggml_tensor * second; - { - second = ggml_view_4d(ctx0, cur, - n_dim/2, n_head, n_pos, n_batch, - cur->nb[1], - cur->nb[2], - cur->nb[3], - n_dim/2 * ggml_element_size(cur)); - second = ggml_rope_ext( - ctx0, - second, - pos_y, // positions - nullptr, // freq factors - n_dim/2, // n_dims - GGML_ROPE_TYPE_NEOX, 0, hparams.rope_theta, - 1.0f, 0.0f, 1.0f, 0.0f, 0.0f - ); - } + // second half, dims [n_dim/2, n_dim) + cur = ggml_rope_ext( + ctx0, + cur, + pos_y, // positions + nullptr, // freq factors + n_dim/2, // n_dims + GGML_ROPE_TYPE_NEOX, 0, hparams.rope_theta, + 1.0f, 0.0f, 1.0f, 0.0f, 0.0f + ); + cur = ggml_rope_set_offset(cur, n_dim/2); - cur = ggml_concat(ctx0, first, second, 0); return cur; }; diff --git a/tools/mtmd/models/minimax-m3.cpp b/tools/mtmd/models/minimax-m3.cpp index 447621754..256e53105 100644 --- a/tools/mtmd/models/minimax-m3.cpp +++ b/tools/mtmd/models/minimax-m3.cpp @@ -2,30 +2,22 @@ ggml_tensor * clip_graph_minimax_m3::apply_rope( ggml_tensor * x, ggml_tensor * pos_h, ggml_tensor * pos_w) { - const int64_t Hn = x->ne[1]; - const int64_t P = x->ne[2]; - const size_t es = ggml_element_size(x); - const int dh = (int) x->ne[0]; - const int axd = 2 * ((2 * (dh / 2) / 3) / 2); + const int dh = (int) x->ne[0]; + const int axd = 2 * ((2 * (dh / 2) / 3) / 2); - GGML_ASSERT(x->nb[0] == es); GGML_ASSERT(3 * axd <= dh); const float th = hparams.rope_theta; // layout of x is [t, h, w, pad] // t is unrotated, h and w are rotated, pad is unrotated - // note: everything from n_dims onward untouched, so w and pad are rotated in one call. - auto sl = [&](int off, int n) { - return ggml_cont(ctx0, ggml_view_3d(ctx0, x, n, Hn, P, x->nb[1], x->nb[2], (size_t) off * es)); - }; - ggml_tensor * t = sl(0, axd); - ggml_tensor * h = sl(axd, axd); - ggml_tensor * w = sl(2 * axd, dh - 2 * axd); // w + pad + x = ggml_rope_ext(ctx0, x, pos_h, nullptr, axd, GGML_ROPE_TYPE_NEOX, 0, th, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); + x = ggml_rope_set_offset(x, axd); - h = ggml_rope_ext(ctx0, h, pos_h, nullptr, axd, GGML_ROPE_TYPE_NEOX, 0, th, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); - w = ggml_rope_ext(ctx0, w, pos_w, nullptr, axd, GGML_ROPE_TYPE_NEOX, 0, th, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); - return ggml_concat(ctx0, ggml_concat(ctx0, t, h, 0), w, 0); + x = ggml_rope_ext(ctx0, x, pos_w, nullptr, axd, GGML_ROPE_TYPE_NEOX, 0, th, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); + x = ggml_rope_set_offset(x, 2 * axd); + + return x; } ggml_cgraph * clip_graph_minimax_m3::build() { From 3f545beccee69d9975f466ec7e45fd9aacd8ba90 Mon Sep 17 00:00:00 2001 From: Safi Ullah Date: Sun, 23 Aug 2026 00:42:20 +0500 Subject: [PATCH 03/28] vulkan : added the PAD_REFLECT_1D operation (#26586) * vulkan : added PAD_REFLECT_1D operation Implemented the GGML_OP_PAD_REFLECT_1D operation for the Vulkan backend Changes: - pad_reflect_1d.comp: implemented the GLSL compute shader with reflection logic - vulkan-shaders-gen.cpp: register the shader for SPIR-V compilation - ggml-vulkan.cpp: pushed constants struct, pipeline creation, supports_op, dispatch function, compute switch and debug validation Tested the PAD_REFLECT_1D on Intel Iris Xe (Vulkan 1.4, Mesa 25.2.8): Correctness: PAD_REFLECT_1D(type=f32,ne_a=[512,34,2,1],pad_0=10,pad_1=9) = Pass PAD_REFLECT_1D(type=f32,ne_a=[3000,384,4,1],pad_0=10,pad_1=9) = Pass 2/2 tests passed - All test are passed Performance: ne_a=[512,34,2,1] -> 5.38 us/run, 24.55 GB/s ne_a=[3000,80,1,1] -> 30.09 us/run, 59.62 GB/s ne_a=[3000,384,4,1] -> 158.31 us/run, 54.39 GB/s * Update ggml/src/ggml-vulkan/vulkan-shaders/pad_reflect_1d.comp Co-authored-by: Jeff Bolz --------- Co-authored-by: Jeff Bolz --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 26 +++++++++++ .../vulkan-shaders/pad_reflect_1d.comp | 43 +++++++++++++++++++ .../vulkan-shaders/vulkan-shaders-gen.cpp | 1 + 3 files changed, 70 insertions(+) create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/pad_reflect_1d.comp diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index f6cbaecb7..c1d86aaac 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -955,6 +955,7 @@ struct vk_device_struct { vk_pipeline pipeline_diag[2]; vk_pipeline pipeline_clamp[2]; vk_pipeline pipeline_pad_f32; + vk_pipeline pipeline_pad_reflect_1d_f32; vk_pipeline pipeline_roll_f32; vk_pipeline pipeline_repeat_i32, pipeline_repeat_back_f32; vk_pipeline pipeline_repeat_i16; @@ -5630,6 +5631,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { ggml_vk_create_pipeline(device, device->pipeline_diag[1], "diag_f16", diag_f16_len, diag_f16_data, "main", 2, sizeof(vk_op_unary_push_constants), {512, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_pad_f32, "pad_f32", pad_f32_len, pad_f32_data, "main", 2, sizeof(vk_op_pad_push_constants), {512, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_pad_reflect_1d_f32, "pad_reflect_1d_f32", pad_reflect_1d_f32_len, pad_reflect_1d_f32_data, "main", 2, sizeof(vk_op_unary_push_constants), {512, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_roll_f32, "roll_f32", roll_f32_len, roll_f32_data, "main", 2, sizeof(vk_op_unary_push_constants), {512, 1, 1}, {}, 1); @@ -11336,6 +11338,11 @@ static vk_pipeline ggml_vk_op_get_pipeline(ggml_backend_vk_context * ctx, const return ctx->device->pipeline_pad_f32; } return nullptr; + case GGML_OP_PAD_REFLECT_1D: + if (src0->type == GGML_TYPE_F32 && dst->type == GGML_TYPE_F32) { + return ctx->device->pipeline_pad_reflect_1d_f32; + } + return nullptr; case GGML_OP_ROLL: if (src0->type == GGML_TYPE_F32 && dst->type == GGML_TYPE_F32) { return ctx->device->pipeline_roll_f32; @@ -12239,6 +12246,7 @@ static void ggml_vk_op_f32(ggml_backend_vk_context * ctx, vk_context& subctx, co case GGML_OP_CLAMP: case GGML_OP_LEAKY_RELU: case GGML_OP_PAD: + case GGML_OP_PAD_REFLECT_1D: case GGML_OP_ROLL: case GGML_OP_REPEAT: case GGML_OP_REPEAT_BACK: @@ -13111,6 +13119,17 @@ static void ggml_vk_pad(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_vk_op_f32(ctx, subctx, src0, nullptr, nullptr, nullptr, dst, GGML_OP_PAD, std::move(p)); } +static void ggml_vk_pad_reflect_1d(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * src0, ggml_tensor * dst) { + const uint32_t p0 = (uint32_t)dst->op_params[0]; + const uint32_t p1 = (uint32_t)dst->op_params[1]; + + vk_op_unary_push_constants p = vk_op_unary_push_constants_init(src0, dst, ggml_nelements(dst)); + memcpy(&p.param1, &p0, sizeof(float)); + memcpy(&p.param2, &p1, sizeof(float)); + + ggml_vk_op_f32(ctx, subctx, src0, nullptr, nullptr, nullptr, dst, GGML_OP_PAD_REFLECT_1D, std::move(p)); +} + static void ggml_vk_roll(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * src0, ggml_tensor * dst) { const int32_t s0 = ggml_get_op_params_i32(dst, 0); const int32_t s1 = ggml_get_op_params_i32(dst, 1); @@ -15520,6 +15539,10 @@ static bool ggml_vk_build_graph(ggml_backend_vk_context * ctx, ggml_cgraph * cgr case GGML_OP_PAD: ggml_vk_pad(ctx, compute_ctx, src0, node); + break; + case GGML_OP_PAD_REFLECT_1D: + ggml_vk_pad_reflect_1d(ctx, compute_ctx, src0, node); + break; case GGML_OP_ROLL: ggml_vk_roll(ctx, compute_ctx, src0, node); @@ -18446,6 +18469,7 @@ static bool ggml_backend_vk_device_supports_op(ggml_backend_dev_t dev, const ggm case GGML_OP_SCALE: return ggml_is_contiguous(op->src[0]) && op->src[0]->type == GGML_TYPE_F32; case GGML_OP_PAD: + case GGML_OP_PAD_REFLECT_1D: case GGML_OP_ROLL: return op->src[0]->type == GGML_TYPE_F32; case GGML_OP_DIAG_MASK_INF: @@ -19228,6 +19252,8 @@ static void ggml_vk_check_results_0(ggml_backend_vk_context * ctx, ggml_cgraph * } else if (tensor->op == GGML_OP_PAD) { tensor_clone = ggml_pad_ext(ggml_ctx, src_clone[0], tensor->op_params[0], tensor->op_params[1], tensor->op_params[2], tensor->op_params[3], tensor->op_params[4], tensor->op_params[5], tensor->op_params[6], tensor->op_params[7]); + } else if (tensor->op == GGML_OP_PAD_REFLECT_1D) { + tensor_clone = ggml_pad_reflect_1d(ggml_ctx, src_clone[0], tensor->op_params[0], tensor->op_params[1]); } else if (tensor->op == GGML_OP_REPEAT) { tensor_clone = ggml_repeat(ggml_ctx, src_clone[0], tensor); } else if (tensor->op == GGML_OP_REPEAT_BACK) { diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/pad_reflect_1d.comp b/ggml/src/ggml-vulkan/vulkan-shaders/pad_reflect_1d.comp new file mode 100644 index 000000000..2389020fa --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/pad_reflect_1d.comp @@ -0,0 +1,43 @@ +#version 450 + +#include "types.glsl" +#include "generic_unary_head.glsl" // included to use functions like fastdiv etc. + +layout(local_size_x = 512, local_size_y = 1, local_size_z = 1) in; + +void main() { + + const uint idx = get_idx(); + + if (idx >= p.ne) { + return; + } + + const uint p0 = floatBitsToUint(p.param1); + const uint p1 = floatBitsToUint(p.param2); + + const uint i3 = fastdiv(idx, p.ne1_012mp, fastdiv_L(p.ne1_Ls, 0)); + const uint i3_offset = i3 * p.ne12 * p.ne11 * p.ne10; + + const uint i2 = fastdiv(idx - i3_offset, p.ne1_01mp, fastdiv_L(p.ne1_Ls, 1)); + const uint i2_offset = i2 * p.ne11 * p.ne10; + + const uint i1 = fastdiv(idx - i3_offset - i2_offset, p.ne1_0mp, fastdiv_L(p.ne1_Ls, 2)); + const uint i0 = idx - i3_offset - i2_offset - i1 * p.ne10; + + uint src_col; + + if (i0 < p0) { + src_col = p0 - i0; // left pad area + } else if (i0 < p0 + p.ne00) { + src_col = i0 - p0; // center area + } else { + src_col = 2u * p.ne00 - 2u - (i0 - p0); // right pad area + } + + const uint src_idx = i3 * p.nb03 + i2 * p.nb02 + i1 * p.nb01 + src_col * p.nb00; + const uint d_idx = i3 * p.nb13 + i2 * p.nb12 + i1 * p.nb11 + i0 * p.nb10; + + // copy the computed value to the destination tensor + data_d[get_doffset() + d_idx] = D_TYPE(data_a[get_aoffset() + src_idx]); +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp index caa0c889a..17d57d5a1 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp @@ -896,6 +896,7 @@ void process_shaders() { string_to_spv("scale_f32", "scale.comp", {{"A_TYPE", "float"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}}); string_to_spv("pad_f32", "pad.comp", {{"A_TYPE", "float"}, {"D_TYPE", "float"}}); + string_to_spv("pad_reflect_1d_f32", "pad_reflect_1d.comp", {{"A_TYPE", "float"}, {"D_TYPE", "float"}}); string_to_spv("concat_i8", "concat.comp", {{"A_TYPE", "uint8_t"}, {"B_TYPE", "uint8_t"}, {"D_TYPE", "uint8_t"}}); string_to_spv("concat_i16", "concat.comp", {{"A_TYPE", "uint16_t"}, {"B_TYPE", "uint16_t"}, {"D_TYPE", "uint16_t"}}); From 70adb1b4cea5ee39f867792c78dc59320921eda7 Mon Sep 17 00:00:00 2001 From: Xuan-Son Nguyen Date: Sun, 23 Aug 2026 01:11:10 +0200 Subject: [PATCH 04/28] common: json.h: fix clang lto (#27575) --- common/json.cpp | 48 ++++++++++++++++++++++-------------------------- common/json.h | 6 ++---- 2 files changed, 24 insertions(+), 30 deletions(-) diff --git a/common/json.cpp b/common/json.cpp index 547542bb7..37713cef2 100644 --- a/common/json.cpp +++ b/common/json.cpp @@ -78,19 +78,21 @@ common_json_value::common_json_value(const common_json & val) : common_json_value::common_json_value(common_json && val) : type(VAL_JSON), val_json(std::make_shared(std::move(val))) {} +// the ctors and get() below are explicit specializations, giving strong symbols +// an explicit instantiation is a weak symbol, dropped by some LTO builds (clang-cl) template -common_json_value::common_json_value(const std::set & vals) : type(VAL_JSON) { +static std::shared_ptr set_json(const std::set & vals) { common_json out = common_json::array(); for (const auto & val : vals) { out.push_back(val); } - val_json = std::make_shared(std::move(out)); + return std::make_shared(std::move(out)); } // a set value is usable only for the types below -#define COMMON_JSON_SET(...) template common_json_value::common_json_value(const std::set<__VA_ARGS__> &); +#define COMMON_JSON_SET(...) template <> common_json_value::common_json_value(const std::set<__VA_ARGS__> & vals) : type(VAL_JSON), val_json(set_json(vals)) {} COMMON_JSON_SET(int) COMMON_JSON_SET(std::string) @@ -98,56 +100,45 @@ COMMON_JSON_SET(std::string) #undef COMMON_JSON_SET template -common_json_value::common_json_value(const std::map & vals) : type(VAL_JSON) { +static std::shared_ptr map_json(const T & vals) { common_json out = common_json::object(); for (const auto & val : vals) { out.set({ val.first, val.second }); } - val_json = std::make_shared(std::move(out)); + return std::make_shared(std::move(out)); } // a map value is usable only for the types below -#define COMMON_JSON_MAP(...) template common_json_value::common_json_value(const std::map &); +#define COMMON_JSON_MAP(...) template <> common_json_value::common_json_value(const std::map & vals) : type(VAL_JSON), val_json(map_json(vals)) {} COMMON_JSON_MAP(bool) COMMON_JSON_MAP(std::string) #undef COMMON_JSON_MAP -template -common_json_value::common_json_value(const std::unordered_map & vals) : type(VAL_JSON) { - common_json out = common_json::object(); - - for (const auto & val : vals) { - out.set({ val.first, val.second }); - } - - val_json = std::make_shared(std::move(out)); -} - // an unordered map value is usable only for the types below -#define COMMON_JSON_UMAP(...) template common_json_value::common_json_value(const std::unordered_map &); +#define COMMON_JSON_UMAP(...) template <> common_json_value::common_json_value(const std::unordered_map & vals) : type(VAL_JSON), val_json(map_json(vals)) {} COMMON_JSON_UMAP(size_t) #undef COMMON_JSON_UMAP template -common_json_value::common_json_value(const std::vector & vals) : type(VAL_JSON) { +static std::shared_ptr vec_json(const std::vector & vals) { common_json out = common_json::array(); for (const auto & val : vals) { out.push_back(val); } - val_json = std::make_shared(std::move(out)); + return std::make_shared(std::move(out)); } // a vector value is usable only for the types below // note: std::vector is not here, its proxy reference does not convert -#define COMMON_JSON_VEC(...) template common_json_value::common_json_value(const std::vector<__VA_ARGS__> &); +#define COMMON_JSON_VEC(...) template <> common_json_value::common_json_value(const std::vector<__VA_ARGS__> & vals) : type(VAL_JSON), val_json(vec_json(vals)) {} COMMON_JSON_VEC(int) COMMON_JSON_VEC(unsigned char) @@ -404,10 +395,6 @@ common_json::items_view common_json::items() const { return items_view(const_cast(this), size()); } -template T common_json::get() const { - return guard([&] { return as_json(this).get(); }); -} - // the backing library cannot build a common_json, so this one is just a copy template <> common_json common_json::get() const { return *this; @@ -415,7 +402,7 @@ template <> common_json common_json::get() const { // get() is usable only for the types below -#define COMMON_JSON_GET(...) template __VA_ARGS__ common_json::get<__VA_ARGS__>() const; +#define COMMON_JSON_GET(...) template <> __VA_ARGS__ common_json::get<__VA_ARGS__>() const { return guard([&] { return as_json(this).get<__VA_ARGS__>(); }); } COMMON_JSON_GET(bool) COMMON_JSON_GET(int) @@ -435,3 +422,12 @@ COMMON_JSON_GET(std::vector) COMMON_JSON_GET(std::unordered_map) #undef COMMON_JSON_GET + +// must stay below the get specialization +common_json::operator std::string() const { + return get(); +} + +std::string common_json::value(const std::string & key, const char * def) const { + return contains(key) ? at(key).get() : std::string(def); +} diff --git a/common/json.h b/common/json.h index 9e20a2adb..f3ad4edee 100644 --- a/common/json.h +++ b/common/json.h @@ -221,16 +221,14 @@ class common_json { // implicit get() for plain values, so they can be assigned to their C++ type directly // note: kept to this short list on purpose, a wider one makes j["key"] ambiguous // note: a numeric one would make "str = json;" ambiguous, a number converts to char too - operator std::string() const { return get(); } + operator std::string() const; template T value(const std::string & key, T def) const { return contains(key) ? at(key).get() : def; } - std::string value(const std::string & key, const char * def) const { - return contains(key) ? at(key).get() : std::string(def); - } + std::string value(const std::string & key, const char * def) const; // a JSON default needs no get(), it is already the right type common_json value(const std::string & key, const common_json & def) const { From 29ea9412a6b5e343dc6736a5a4349158cf78b30c Mon Sep 17 00:00:00 2001 From: Aman Karki Date: Sun, 23 Aug 2026 13:07:32 +0530 Subject: [PATCH 05/28] cuda : add POOL_1D support (#27573) * cuda : add POOL_1D support * fix: add missing trailing newline for editorconfig compliance --- ggml/src/ggml-cuda/ggml-cuda.cu | 5 ++ ggml/src/ggml-cuda/pool1d.cu | 85 +++++++++++++++++++++++++++++++++ ggml/src/ggml-cuda/pool1d.cuh | 5 ++ 3 files changed, 95 insertions(+) create mode 100644 ggml/src/ggml-cuda/pool1d.cu create mode 100644 ggml/src/ggml-cuda/pool1d.cuh diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu index a8a1c09ca..b4c128141 100644 --- a/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/ggml/src/ggml-cuda/ggml-cuda.cu @@ -38,6 +38,7 @@ #include "ggml-cuda/out-prod.cuh" #include "ggml-cuda/pad.cuh" #include "ggml-cuda/pool2d.cuh" +#include "ggml-cuda/pool1d.cuh" #include "ggml-cuda/quantize.cuh" #include "ggml-cuda/rope.cuh" #include "ggml-cuda/roll.cuh" @@ -2326,6 +2327,9 @@ static bool ggml_cuda_compute_forward(ggml_backend_cuda_context & ctx, struct gg case GGML_OP_POOL_2D: ggml_cuda_op_pool2d(ctx, dst); break; + case GGML_OP_POOL_1D: + ggml_cuda_op_pool1d(ctx, dst); + break; case GGML_OP_SUM: ggml_cuda_op_sum(ctx, dst); break; @@ -5245,6 +5249,7 @@ static bool ggml_backend_cuda_device_supports_op(ggml_backend_dev_t dev, const g case GGML_OP_CONV_2D_DW: return op->src[0]->type == GGML_TYPE_F32; case GGML_OP_CONV_TRANSPOSE_2D: + case GGML_OP_POOL_1D: case GGML_OP_POOL_2D: return true; case GGML_OP_ACC: diff --git a/ggml/src/ggml-cuda/pool1d.cu b/ggml/src/ggml-cuda/pool1d.cu new file mode 100644 index 000000000..ac6fb0cbd --- /dev/null +++ b/ggml/src/ggml-cuda/pool1d.cu @@ -0,0 +1,85 @@ +#include "pool1d.cuh" + +static __global__ void pool1d_nchw_kernel( + const int iw, const int ow, + const int kw, const int sw, const int pw, + const int parallel_elements, + const float * src, float * dst, const enum ggml_op_pool op) { + const int idx = threadIdx.x + blockIdx.x * blockDim.x; + if (idx >= parallel_elements) { + return; + } + + const int nc = idx / ow; + const int cur_ow = idx % ow; + + const float * i_ptr = src + nc * iw; + float * o_ptr = dst + nc * ow; + + const int start = cur_ow * sw - pw; + const int b = max(0, start); + const int e = min(iw, start + kw); + + float res; + switch (op) { + case GGML_OP_POOL_AVG: res = 0.0f; break; + case GGML_OP_POOL_MAX: res = -FLT_MAX; break; + default: return; + } + + int count = 0; + for (int i = b; i < e; i++) { +#if __CUDA_ARCH__ >= 350 + float cur = __ldg(i_ptr + i); +#else + float cur = i_ptr[i]; +#endif + switch (op) { + case GGML_OP_POOL_AVG: res += cur; break; + case GGML_OP_POOL_MAX: res = max(res, cur); break; + default: break; + } + count++; + } + + if (op == GGML_OP_POOL_AVG) { + res = (count > 0) ? (res / count) : 0.0f; + } + + o_ptr[cur_ow] = res; +} + +static void pool1d_nchw_kernel_f32_f32_cuda( + const int iw, const int ow, + const int kw, const int sw, const int pw, + const int parallel_elements, + const float * src, float * dst, const enum ggml_op_pool op, + cudaStream_t stream) { + const int num_blocks = (parallel_elements + CUDA_POOL1D_BLOCK_SIZE - 1) / CUDA_POOL1D_BLOCK_SIZE; + dim3 block_nums(num_blocks); + pool1d_nchw_kernel<<>>(iw, ow, kw, sw, pw, parallel_elements, src, dst, op); +} + +void ggml_cuda_op_pool1d(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { + const ggml_tensor * src0 = dst->src[0]; + const float * src0_d = (const float *)src0->data; + float * dst_d = (float *)dst->data; + cudaStream_t stream = ctx.stream(); + + GGML_ASSERT(src0->type == GGML_TYPE_F32); + GGML_ASSERT( dst->type == GGML_TYPE_F32); + + const int32_t * opts = (const int32_t *)dst->op_params; + enum ggml_op_pool op = static_cast(opts[0]); + const int k0 = opts[1]; + const int s0 = opts[2]; + const int p0 = opts[3]; + + const int64_t IW = src0->ne[0]; + const int64_t OW = dst->ne[0]; + const int64_t nr = ggml_nrows(src0); + + const int parallel_elements = (int)(nr * OW); + + pool1d_nchw_kernel_f32_f32_cuda(IW, OW, k0, s0, p0, parallel_elements, src0_d, dst_d, op, stream); +} diff --git a/ggml/src/ggml-cuda/pool1d.cuh b/ggml/src/ggml-cuda/pool1d.cuh new file mode 100644 index 000000000..c79461dd8 --- /dev/null +++ b/ggml/src/ggml-cuda/pool1d.cuh @@ -0,0 +1,5 @@ +#include "common.cuh" + +#define CUDA_POOL1D_BLOCK_SIZE 256 + +void ggml_cuda_op_pool1d(ggml_backend_cuda_context & ctx, ggml_tensor * dst); From 6657ded4faa3b8450221119fc6b4d002e35104a2 Mon Sep 17 00:00:00 2001 From: "Alessandro de Oliveira Faria (A.K.A.CABELO)" Date: Sun, 23 Aug 2026 04:38:29 -0300 Subject: [PATCH 06/28] vendor : update subprocess.h (#27409) --- scripts/sync_vendor.py | 2 +- vendor/sheredom/subprocess.h | 375 +++++++++++++++++++++++++++++++++-- 2 files changed, 364 insertions(+), 13 deletions(-) diff --git a/scripts/sync_vendor.py b/scripts/sync_vendor.py index 18a94e1c6..98b9ddc8e 100755 --- a/scripts/sync_vendor.py +++ b/scripts/sync_vendor.py @@ -27,7 +27,7 @@ vendor = { f"https://raw.githubusercontent.com/yhirose/cpp-httplib/{HTTPLIB_VERSION}/split.py": "split.py", f"https://raw.githubusercontent.com/yhirose/cpp-httplib/{HTTPLIB_VERSION}/LICENSE": "vendor/cpp-httplib/LICENSE", - "https://raw.githubusercontent.com/sheredom/subprocess.h/9ce0d701b6fb10f8f8c4445edd31e7c60a1237e3/subprocess.h": "vendor/sheredom/subprocess.h", + "https://raw.githubusercontent.com/sheredom/subprocess.h/0dccaa9aa176dd6d7ef8afeca3c18d6e80a32795/subprocess.h": "vendor/sheredom/subprocess.h", f"https://raw.githubusercontent.com/Cyan4973/xxHash/{XXHASH_COMMIT}/xxhash.c": "vendor/hash/xxhash/xxhash.c", f"https://raw.githubusercontent.com/Cyan4973/xxHash/{XXHASH_COMMIT}/xxhash.h": "vendor/hash/xxhash/xxhash.h", diff --git a/vendor/sheredom/subprocess.h b/vendor/sheredom/subprocess.h index c3af8a498..67420c103 100644 --- a/vendor/sheredom/subprocess.h +++ b/vendor/sheredom/subprocess.h @@ -275,13 +275,46 @@ subprocess_weak int subprocess_alive(struct subprocess_s *const process); #include #endif +#if defined(__NetBSD__) +#include +#endif + +/* Which spelling of the chdir file action the platform provides, if any. + POSIX 2024 standardised posix_spawn_file_actions_addchdir; implementations + that shipped it earlier called it ..._np. macOS 26 and NetBSD 10 use the + standard name, glibc 2.29+, macOS 10.15+ and FreeBSD 13.1+ use the _np name, + and AIX, NetBSD 9 and older, and OpenBSD provide neither. */ +#if !defined(SUBPROCESS_ADDCHDIR_IS_POSIX) +#if (defined(__APPLE__) && MAC_OS_X_VERSION_MIN_REQUIRED >= 260000) || \ + (defined(__NetBSD__) && __NetBSD_Version__ >= 1000000000) +#define SUBPROCESS_ADDCHDIR_IS_POSIX 1 +#else +#define SUBPROCESS_ADDCHDIR_IS_POSIX 0 +#endif +#endif + +/* Whether to launch the child with fork()+exec() instead of posix_spawn(), + for platforms with no posix_spawn_file_actions_addchdir under either + spelling: the child chdir()s before exec, and a close-on-exec pipe carries + exec's errno back. Define this yourself to force either implementation. */ +#if !defined(SUBPROCESS_SPAWN_VIA_FORK) +#if defined(_AIX) || defined(__OpenBSD__) || \ + (defined(__NetBSD__) && (__NetBSD_Version__ < 1000000000)) +#define SUBPROCESS_SPAWN_VIA_FORK 1 +#else +#define SUBPROCESS_SPAWN_VIA_FORK 0 +#endif +#endif + /* Whether subprocess_create_ex can honour process_cwd. glibc only gained posix_spawn_file_actions_addchdir_np in 2.29, and macOS in 10.15; the SDKs mark it unavailable on iOS, tvOS and watchOS, where the undefined version macro folds to 0 and so answers correctly. Define this yourself to override the detection, for instance on musl older than 1.1.24. */ #if !defined(SUBPROCESS_HAVE_CWD) -#if defined(__GLIBC__) +#if SUBPROCESS_SPAWN_VIA_FORK +#define SUBPROCESS_HAVE_CWD 1 +#elif defined(__GLIBC__) #if __GLIBC_PREREQ(2, 29) #define SUBPROCESS_HAVE_CWD 1 #else @@ -294,10 +327,13 @@ subprocess_weak int subprocess_alive(struct subprocess_s *const process); #endif #endif -/* Whether posix_spawn reports a failed exec back to the caller. glibc only - started doing so in 2.24; before that the child silently exits with 127. */ +/* Whether a failed exec is reported back to the caller. The fork() path always + reports it through its error pipe. glibc's posix_spawn only started doing so + in 2.24; before that the child silently exits with 127. */ #if !defined(SUBPROCESS_SPAWN_REPORTS_EXEC_ERRORS) -#if defined(__GLIBC__) +#if SUBPROCESS_SPAWN_VIA_FORK +#define SUBPROCESS_SPAWN_REPORTS_EXEC_ERRORS 1 +#elif defined(__GLIBC__) #if __GLIBC_PREREQ(2, 24) #define SUBPROCESS_SPAWN_REPORTS_EXEC_ERRORS 1 #else @@ -342,6 +378,14 @@ typedef intptr_t subprocess_intptr_t; typedef size_t subprocess_size_t; #endif +/* SIZE_T is ULONG_PTR, which is not size_t: on Win32 both are 32 bits wide but + unsigned long and unsigned int are still distinct types. */ +#ifdef _WIN64 +typedef subprocess_size_t subprocess_ulongptr_t; +#else +typedef unsigned long subprocess_ulongptr_t; +#endif + #ifdef __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wreserved-identifier" @@ -351,6 +395,7 @@ typedef struct _PROCESS_INFORMATION *LPPROCESS_INFORMATION; typedef struct _SECURITY_ATTRIBUTES *LPSECURITY_ATTRIBUTES; typedef struct _STARTUPINFOW *LPSTARTUPINFOW; typedef struct _OVERLAPPED *LPOVERLAPPED; +typedef struct _PROC_THREAD_ATTRIBUTE_LIST *LPPROC_THREAD_ATTRIBUTE_LIST; #ifdef __clang__ #pragma clang diagnostic pop @@ -402,6 +447,11 @@ struct subprocess_startup_info_s { void *hStdError; }; +struct subprocess_startup_info_ex_s { + struct subprocess_startup_info_s startupInfo; + void *attributeList; +}; + struct subprocess_overlapped_s { uintptr_t Internal; uintptr_t InternalHigh; @@ -451,6 +501,14 @@ __declspec(dllimport) int __stdcall CreateProcessW( const subprocess_wchar_t *, subprocess_wchar_t *, LPSECURITY_ATTRIBUTES, LPSECURITY_ATTRIBUTES, int, unsigned long, void *, const subprocess_wchar_t *, LPSTARTUPINFOW, LPPROCESS_INFORMATION); +__declspec(dllimport) int __stdcall +InitializeProcThreadAttributeList(LPPROC_THREAD_ATTRIBUTE_LIST, unsigned long, + unsigned long, subprocess_ulongptr_t *); +__declspec(dllimport) int __stdcall UpdateProcThreadAttribute( + LPPROC_THREAD_ATTRIBUTE_LIST, unsigned long, subprocess_ulongptr_t, void *, + subprocess_ulongptr_t, void *, subprocess_ulongptr_t *); +__declspec(dllimport) void __stdcall +DeleteProcThreadAttributeList(LPPROC_THREAD_ATTRIBUTE_LIST); __declspec(dllimport) int __stdcall MultiByteToWideChar( unsigned int, unsigned long, const char *, int, subprocess_wchar_t *, int); __declspec(dllimport) int __stdcall CloseHandle(void *); @@ -667,12 +725,104 @@ int subprocess_create_named_pipe_helper(void **rd, void **wr) { } #endif +#if !defined(_WIN32) +/* Move a pipe end off 0, 1 or 2. Duplicating a descriptor onto itself is a + no-op, so a pipe end already sitting on a standard descriptor would keep its + FD_CLOEXEC and be closed by exec, leaving the child without that stream. */ +static int subprocess_fds_above_std(int fds[2]) { + int fd_flags; + int index; + int moved; + int saved_errno; + + for (index = 0; index < 2; index++) { + if (fds[index] > STDERR_FILENO) { + continue; + } + + moved = fcntl(fds[index], F_DUPFD, STDERR_FILENO + 1); + if (-1 != moved) { + fd_flags = fcntl(moved, F_GETFD, 0); + if ((-1 == fd_flags) || + (-1 == fcntl(moved, F_SETFD, fd_flags | FD_CLOEXEC))) { + saved_errno = errno; + close(moved); + errno = saved_errno; + moved = -1; + } + } + + if (-1 == moved) { + saved_errno = errno; + close(fds[0]); + close(fds[1]); + fds[0] = -1; + fds[1] = -1; + errno = saved_errno; + return -1; + } + + close(fds[index]); + fds[index] = moved; + } + + return 0; +} + +/* Create pipes with close-on-exec set so later subprocesses do not inherit + descriptors belonging to subprocesses which are already running. */ +static int subprocess_pipe_cloexec(int fds[2]) { + int fd_flags; + int index; + int saved_errno; + +#if defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__) || \ + defined(__OpenBSD__) || defined(__DragonFly__) || \ + (defined(__sun) && defined(__SVR4)) + if (0 == pipe2(fds, O_CLOEXEC)) { + return subprocess_fds_above_std(fds); + } + + /* Older kernels can lack pipe2 even when the C library declares it. */ + if (ENOSYS != errno) { + return -1; + } +#endif + + if (0 != pipe(fds)) { + return -1; + } + + for (index = 0; index < 2; index++) { + fd_flags = fcntl(fds[index], F_GETFD, 0); + if ((-1 == fd_flags) || + (-1 == fcntl(fds[index], F_SETFD, fd_flags | FD_CLOEXEC))) { + saved_errno = errno; + close(fds[0]); + close(fds[1]); + fds[0] = -1; + fds[1] = -1; + errno = saved_errno; + return -1; + } + } + + return subprocess_fds_above_std(fds); +} +#endif + int subprocess_create(const char *const commandLine[], int options, struct subprocess_s *const out_process) { return subprocess_create_ex(commandLine, options, SUBPROCESS_NULL, SUBPROCESS_NULL, out_process); } +#if SUBPROCESS_SPAWN_VIA_FORK +/* Not every platform declares execvpe: AIX exports it from libc without ever + naming it in a header, and glibc hides it behind _GNU_SOURCE. */ +extern int execvpe(const char *, char *const *, char *const *); +#endif + int subprocess_create_ex(const char *const commandLine[], int options, const char *const environment[], const char *const process_cwd, @@ -692,6 +842,7 @@ int subprocess_create_ex(const char *const commandLine[], int options, subprocess_size_t bs_run; unsigned long flags = 0; unsigned long last_error = 0; + int attribute_list_initialized = 0; int result = subprocess_error_unknown; const unsigned int codePageUtf8 = 65001; const unsigned long mbErrInvalidChars = 0x00000008; @@ -699,6 +850,8 @@ int subprocess_create_ex(const char *const commandLine[], int options, const unsigned long handleFlagInherit = 0x00000001; const unsigned long createNoWindow = 0x08000000; const unsigned long createUnicodeEnvironment = 0x00000400; + const unsigned long extendedStartupInfoPresent = 0x00080000; + const subprocess_size_t procThreadAttributeHandleList = 0x00020002; struct subprocess_subprocess_information_s processInfo = {SUBPROCESS_NULL, SUBPROCESS_NULL, 0, 0}; @@ -706,6 +859,11 @@ int subprocess_create_ex(const char *const commandLine[], int options, SUBPROCESS_NULL, 1}; subprocess_wchar_t empty_environment[2] = {0, 0}; subprocess_wchar_t *used_environment = SUBPROCESS_NULL; + subprocess_ulongptr_t attribute_list_size = 0; + subprocess_size_t inherited_handle_count = 0; + LPPROC_THREAD_ATTRIBUTE_LIST attribute_list = SUBPROCESS_NULL; + void *inherited_handles[3]; + struct subprocess_startup_info_ex_s startInfoEx; struct subprocess_startup_info_s startInfo = {0, SUBPROCESS_NULL, SUBPROCESS_NULL, @@ -1080,6 +1238,44 @@ int subprocess_create_ex(const char *const commandLine[], int options, } } + /* Restrict inheritance to this subprocess's standard streams. Without a + handle list, concurrent subprocess_create calls can inherit each other's + temporarily-inheritable child pipe handles. */ + inherited_handles[inherited_handle_count++] = startInfo.hStdInput; + inherited_handles[inherited_handle_count++] = startInfo.hStdOutput; + if (startInfo.hStdError != startInfo.hStdOutput) { + inherited_handles[inherited_handle_count++] = startInfo.hStdError; + } + + InitializeProcThreadAttributeList(SUBPROCESS_NULL, 1, 0, + &attribute_list_size); + if (0 == attribute_list_size) { + result = subprocess_error_spawn; + goto cleanup; + } + + attribute_list = SUBPROCESS_PTR_CAST(LPPROC_THREAD_ATTRIBUTE_LIST, + _alloca(attribute_list_size)); + if (!attribute_list || !InitializeProcThreadAttributeList( + attribute_list, 1, 0, &attribute_list_size)) { + result = subprocess_error_spawn; + goto cleanup; + } + attribute_list_initialized = 1; + + if (!UpdateProcThreadAttribute( + attribute_list, 0, procThreadAttributeHandleList, inherited_handles, + inherited_handle_count * sizeof(inherited_handles[0]), + SUBPROCESS_NULL, SUBPROCESS_NULL)) { + result = subprocess_error_spawn; + goto cleanup; + } + + startInfoEx.startupInfo = startInfo; + startInfoEx.startupInfo.cb = sizeof(startInfoEx); + startInfoEx.attributeList = attribute_list; + flags |= extendedStartupInfoPresent; + if (!CreateProcessW( SUBPROCESS_NULL, commandLineCombinedWide, // command line @@ -1090,7 +1286,7 @@ int subprocess_create_ex(const char *const commandLine[], int options, used_environment, // used environment process_cwd_wide, // use specified current directory SUBPROCESS_PTR_CAST(LPSTARTUPINFOW, - &startInfo), // STARTUPINFO pointer + &startInfoEx), // STARTUPINFOEX pointer SUBPROCESS_PTR_CAST(LPPROCESS_INFORMATION, &processInfo))) { result = subprocess_error_from_windows_error(GetLastError()); if (subprocess_error_unknown == result) { @@ -1099,6 +1295,9 @@ int subprocess_create_ex(const char *const commandLine[], int options, goto cleanup; } + DeleteProcThreadAttributeList(attribute_list); + attribute_list_initialized = 0; + out_process->hProcess = processInfo.hProcess; processInfo.hProcess = SUBPROCESS_NULL; @@ -1128,6 +1327,10 @@ int subprocess_create_ex(const char *const commandLine[], int options, cleanup: last_error = GetLastError(); + if (attribute_list_initialized) { + DeleteProcThreadAttributeList(attribute_list); + } + if (subprocess_error_unknown == result) { result = subprocess_error_from_windows_error(last_error); } @@ -1173,15 +1376,20 @@ cleanup: int stderrfd[2] = {-1, -1}; int fd, fd_flags; int async_no_wait; - int actions_created = 0; int result = subprocess_error_unknown; int saved_errno = 0; - int posix_error; pid_t child = 0; extern char **environ; char *const empty_environment[1] = {SUBPROCESS_NULL}; - posix_spawn_file_actions_t actions; char *const *used_environment; +#if SUBPROCESS_SPAWN_VIA_FORK + /* Pipe used to relay the child's exec() errno back to the parent. */ + int exec_errfd[2] = {-1, -1}; +#else + int actions_created = 0; + int posix_error; + posix_spawn_file_actions_t actions; +#endif async_no_wait = subprocess_option_enable_async_no_wait == (options & subprocess_option_enable_async_no_wait); @@ -1202,13 +1410,13 @@ cleanup: memset(out_process, 0, sizeof(*out_process)); - if (0 != pipe(stdinfd)) { + if (0 != subprocess_pipe_cloexec(stdinfd)) { saved_errno = errno; result = subprocess_error_pipe; goto cleanup; } - if (0 != pipe(stdoutfd)) { + if (0 != subprocess_pipe_cloexec(stdoutfd)) { saved_errno = errno; result = subprocess_error_pipe; goto cleanup; @@ -1216,7 +1424,7 @@ cleanup: if (subprocess_option_combined_stdout_stderr != (options & subprocess_option_combined_stdout_stderr)) { - if (0 != pipe(stderrfd)) { + if (0 != subprocess_pipe_cloexec(stderrfd)) { saved_errno = errno; result = subprocess_error_pipe; goto cleanup; @@ -1240,6 +1448,136 @@ cleanup: used_environment = empty_environment; } +#if SUBPROCESS_SPAWN_VIA_FORK + /* fork()+exec() instead of posix_spawn, so the child can chdir() first. + exec_errfd[1] is close-on-exec: a successful exec closes it and the parent + reads EOF; a failed exec writes errno through it before _exit. */ + if (0 != pipe(exec_errfd)) { + saved_errno = errno; + result = subprocess_error_pipe; + goto cleanup; + } + + if (-1 == fcntl(exec_errfd[1], F_SETFD, FD_CLOEXEC)) { + saved_errno = errno; + result = subprocess_error_spawn; + goto cleanup; + } + + child = fork(); + + if (child < 0) { + saved_errno = errno; + result = subprocess_error_spawn; + goto cleanup; + } + + if (0 == child) { + /* Child. Everything below must stay async-signal-safe: after fork() in a + threaded process only such functions may be called before exec. */ + int child_errno; + + close(exec_errfd[0]); + + if ((-1 == dup2(stdinfd[0], STDIN_FILENO)) || + (-1 == dup2(stdoutfd[1], STDOUT_FILENO))) { + goto child_failed; + } + + if (subprocess_option_combined_stdout_stderr == + (options & subprocess_option_combined_stdout_stderr)) { + if (-1 == dup2(STDOUT_FILENO, STDERR_FILENO)) { + goto child_failed; + } + } else { + if (-1 == dup2(stderrfd[1], STDERR_FILENO)) { + goto child_failed; + } + } + + /* The originals are only closed once they have been duplicated, so that a + pipe end that already sits on 0, 1 or 2 is not closed out from under us. */ + if (stdinfd[0] > STDERR_FILENO) { + close(stdinfd[0]); + } + if (stdinfd[1] > STDERR_FILENO) { + close(stdinfd[1]); + } + if (stdoutfd[0] > STDERR_FILENO) { + close(stdoutfd[0]); + } + if (stdoutfd[1] > STDERR_FILENO) { + close(stdoutfd[1]); + } + if (stderrfd[0] > STDERR_FILENO) { + close(stderrfd[0]); + } + if (stderrfd[1] > STDERR_FILENO) { + close(stderrfd[1]); + } + + if (process_cwd && (0 != chdir(process_cwd))) { + goto child_failed; + } + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wcast-qual" +#pragma clang diagnostic ignored "-Wold-style-cast" +#endif + if (subprocess_option_search_user_path == + (options & subprocess_option_search_user_path)) { + execvpe(commandLine[0], + SUBPROCESS_CONST_CAST(char *const *, commandLine), + SUBPROCESS_CONST_CAST(char *const *, used_environment)); + } else { + execve(commandLine[0], + SUBPROCESS_CONST_CAST(char *const *, commandLine), + SUBPROCESS_CONST_CAST(char *const *, used_environment)); + } +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + + child_failed: + child_errno = errno; + /* Nothing useful can be done if this write fails; the parent then sees EOF + and reports success, exactly as posix_spawn would without exec reporting. */ + (void)!write(exec_errfd[1], &child_errno, sizeof(child_errno)); + /* 127 is what POSIX requires posix_spawn's child to exit with when exec + fails, so both implementations look the same to a caller. */ + _exit(127); + } + + /* Parent. */ + close(exec_errfd[1]); + exec_errfd[1] = -1; + + { + int child_errno = 0; + ssize_t bytes_read; + + do { + bytes_read = read(exec_errfd[0], &child_errno, sizeof(child_errno)); + } while ((-1 == bytes_read) && (EINTR == errno)); + + close(exec_errfd[0]); + exec_errfd[0] = -1; + + if (bytes_read == (ssize_t)sizeof(child_errno)) { + /* exec failed in the child. Reap it and surface the reason. */ + while ((-1 == waitpid(child, SUBPROCESS_NULL, 0)) && (EINTR == errno)) { + } + child = 0; + saved_errno = child_errno; + result = subprocess_error_from_errno(child_errno); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; + } + } +#else posix_error = posix_spawn_file_actions_init(&actions); if (0 != posix_error) { saved_errno = posix_error; @@ -1253,7 +1591,7 @@ cleanup: // Set working directory if (process_cwd) { -#if defined(__NetBSD__) || (defined(__APPLE__) && MAC_OS_X_VERSION_MIN_REQUIRED >= 260000) +#if SUBPROCESS_ADDCHDIR_IS_POSIX posix_error = posix_spawn_file_actions_addchdir(&actions, process_cwd); #elif !SUBPROCESS_HAVE_CWD posix_error = ENOSYS; @@ -1406,6 +1744,7 @@ cleanup: #ifdef __clang__ #pragma clang diagnostic pop #endif +#endif /* SUBPROCESS_SPAWN_VIA_FORK */ // Close the stdin read end close(stdinfd[0]); @@ -1480,9 +1819,21 @@ cleanup: result = subprocess_error_from_errno(saved_errno); } +#if SUBPROCESS_SPAWN_VIA_FORK + if (-1 != exec_errfd[0]) { + close(exec_errfd[0]); + exec_errfd[0] = -1; + } + + if (-1 != exec_errfd[1]) { + close(exec_errfd[1]); + exec_errfd[1] = -1; + } +#else if (actions_created) { posix_spawn_file_actions_destroy(&actions); } +#endif if (0 != result) { if (child) { From 8144f3192e5a3131cd043f284525e6ceebf82d0f Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Sun, 23 Aug 2026 10:46:49 +0200 Subject: [PATCH 07/28] ui: Chat Conversation Tabbed navigation (#27263) * ui : add browser-style conversation tabs store Track open conversation tabs in order, persisted to localStorage and pruned against the loaded conversation list on init. The chat layout syncs the route's tab on every navigation, so any way of reaching a conversation opens a tab for it. * ui : add temporary new-chat tabs New-chat tabs are unsaved conversations carrying a temporary id used directly as the route (#/chat/). They live in memory and are only persisted to the database - keeping the same id so the route and tab stay stable - when the first message is sent. Deleting one drops it without confirmation, and deleting conversations now closes their tabs. * ui : render conversation tab bar in chat layout Desktop-only tab bar above the chat screen, one tab per open conversation or new-chat tab. The active tab follows the route id; clicking navigates, middle-click or the close button closes (switching to the left neighbor), and a trailing + starts a new chat. Tabs appear only on chat-id routes; the bare #/ new-chat view has none. The bare route stays put unless a prompt/model deep-link routes it to a new-chat tab. * ui : route new-chat entry points through tabs The sidebar New chat item, Cmd+Shift+O, the search page and the arrow-key fallback now open a new-chat tab instead of navigating to the ?new_chat URL, which is removed. New chat is no longer a special route but a tab like any other conversation. * ui : track sidebar expanded state in a shared ui store Move the desktop sidebar expanded/collapsed state out of deviceStore into a dedicated uiStore so the chat tab bar can react to it. Assisted-by: pi * chat : add opt-in conversation tabs setting Add a Display setting that turns browser-style conversation tabs on or off, enabled by default. Assisted-by: pi * chat : add browser-style conversation tabs with a new-chat screen Track open conversations as tabs above the chat, one per open chat, plus a single New chat tab for the bare `#/` route. New chat is just the `#/` screen - no temporary conversations - and its tab is dropped when navigating away. Sending the first message creates a real conversation and opens a tab for it. Assisted-by: pi * chat : turn tab bar into a horizontally scrollable carousel Make the tab bar a horizontally scrollable carousel with edge scroll buttons and active-tab centering, and align its styling with the sidebar. Assisted-by: pi * chat : restyle the scroll-to-bottom button to match tab styling Assisted-by: pi * chat : add close-tab keyboard shortcut Assisted-by: pi * chat : soften tab bar fade and dim inactive tabs Assisted-by: pi * feat: Add stop button to tabs * refactor: Componentize * ui : fix carousel scrollability detection Observe the content wrapper as well as the container, since adding overflowing items does not change the container's own box size. Also expose an onScrollableChange callback. Assisted-by: pi * ui : add unified ScrollCarousel component Single carousel component with top/center variants, gap and scroll options, and hover-revealed chevrons. Rename the HorizontalScrollCarousel accessibility story accordingly. Assisted-by: pi * ui : migrate carousels to ScrollCarousel Switch the settings mobile header, attachments list, thumbnail strip, and MCP resources to the unified component, and drop HorizontalScrollCarousel. Assisted-by: pi * ui : improve chat tabs carousel UX Scroll newly added tabs into view, fade overflowing tabs at the edges, and hide the New chat button while a new-chat tab is open. Assisted-by: pi * refactor: Naming * chat : add keyboard shortcut to jump between conversation tabs Shift+Cmd/Ctrl+Left/Right cycles the open tabs, mirroring the existing Shift+Cmd/Ctrl+Up/Down conversation navigation. Assisted-by: pi * chat : make the whole tab item act as a link The full tab is now a link instead of only the inner label button, while the stop and close buttons stay interactive by swallowing their clicks. Assisted-by: pi * chat : adjust tab bar width and use a shared offset variable Widen the tab bar for the expanded sidebar and rename the tab bar height variable to --chat-tabs-offset with a smaller value so the chat screen min-height accounts for the overlay without overshooting. Assisted-by: pi * chat : account for the tab bar offset in the assistant min-height Subtract the tab bar offset when it is shown so the last assistant message does not overflow the available viewport space. Assisted-by: pi * refactor: Post-review fixes * ui : restore deep links on the chat start page - handle ?model selection, with ?load=true eager router loading - ?q now creates a conversation, sends the prompt, and clears the params - show the not-available-model dialog for unknown models - never block mount on the conversation list Assisted-by: pi * ui : fix tab item link nesting and centralize tab constants - the tab anchor covers the whole item while stop/close stay siblings, so interactive elements are never nested inside the anchor - cmd/ctrl/middle clicks are left to the browser (new window) - extract the tab labels, the active-tab data attribute, and the sidebar-offset max widths into constants Assisted-by: pi * ui : tidy scroll carousel hook and keep mobile header arrows on - drop the dead scrollLeft/scrollRight helpers and the unused onScrollableChange/scrollBy props - init the carousel once instead of inside a derived - restore items-start on the center variant - always show the settings header arrows on touch Assisted-by: pi * ui : keep the new-chat tab across reloads and fall back on close - the new-chat sentinel is no longer pruned on init, so reloading on the bare new-chat route keeps the tab the user is on - closing the active conversation falls back to the new-chat screen when Conversation tabs are off Assisted-by: pi * ui : don't block startup on the conversation list - prune persisted tabs after the list loads in the background instead of awaiting it during init - openNewChat now returns void; its return value was never read Assisted-by: pi * ui: fix routing nits * chore: Update doc comments * refactor: Mark fire-and-forget openNewChat calls as `void` * chat: fix the deep-linked prompt, the tab width and the tab shortcuts The chat start page creates the conversation and hands the prompt over to the chat route, which still sees it in the query string. Sending it on both sides queues the second copy as a pending message, which shows up as a stray user bubble once the answer lands and vanishes on reload since it never reaches the database. The tab bar takes the max width of the collapsed sidebar while it is expanded, and the other way round. The tab list is pruned against a snapshot of the loaded conversations, so a conversation created while that list is still loading loses its tab even though the route just opened it. The active tab then falls out of the list and the cycling shortcut jumps to an edge on every keypress instead of moving one tab over. Tabs synced from the route are kept as they are, only the persisted ones are pruned. The rich chat input claims ctrl or alt with shift and an arrow for its badge-aware word jump, which now belongs to the tab cycling shortcut. Holding shift hands the key combination over, the plain word jump is unchanged. The close-tab shortcut consumes the event before checking whether the setting is on, and the logo background loses its importance flag. --------- Co-authored-by: Pascal --- .../ChatAttachmentsList.svelte | 9 +- ...hatAttachmentsPreviewThumbnailStrip.svelte | 7 +- .../ChatFormInput/ChatFormInputRich.svelte | 2 +- .../ChatForm/ChatFormMcpResourcesList.svelte | 10 +- .../ChatMessageAssistant.svelte | 4 +- .../app/chat/ChatScreen/ChatScreen.svelte | 7 +- .../ChatScreenActionScrollDown.svelte | 2 +- .../app/chat/ChatTabs/ChatTabs.svelte | 136 +++++++++++++++ .../app/chat/ChatTabs/ChatTabsItem.svelte | 156 ++++++++++++++++++ .../ChatTabs/ChatTabsNewChatButton.svelte | 30 ++++ tools/ui/src/lib/components/app/chat/index.ts | 12 ++ .../app/misc/HorizontalScrollCarousel.svelte | 96 ----------- .../components/app/misc/ScrollCarousel.svelte | 131 +++++++++++++++ tools/ui/src/lib/components/app/misc/index.ts | 14 +- .../SidebarNavigation.svelte | 49 +++--- .../SidebarNavigationActions.svelte | 48 ++++-- .../settings/SettingsChatMobileHeader.svelte | 105 +++++------- .../src/lib/constants/chat-tabs.constants.ts | 18 ++ .../lib/constants/css-classes.constants.ts | 6 + tools/ui/src/lib/constants/index.ts | 1 + .../ui/src/lib/constants/routes.constants.ts | 4 - .../lib/constants/settings-keys.constants.ts | 1 + .../src/lib/constants/settings.constants.ts | 7 + .../ui/src/lib/constants/storage.constants.ts | 1 + tools/ui/src/lib/constants/ui.constants.ts | 10 +- tools/ui/src/lib/enums/index.ts | 2 + tools/ui/src/lib/enums/keyboard.enums.ts | 4 +- tools/ui/src/lib/enums/ui.enums.ts | 15 ++ .../hooks/use-keyboard-shortcuts.svelte.ts | 41 ++++- .../lib/hooks/use-scroll-carousel.svelte.ts | 38 +++-- tools/ui/src/lib/services/index.ts | 2 +- .../lib/stores/conversations/index.svelte.ts | 51 ++++-- .../conversations/preferences.svelte.ts | 23 ++- tools/ui/src/lib/stores/index.ts | 6 + tools/ui/src/lib/stores/init.ts | 22 +-- tools/ui/src/lib/stores/tabs.svelte.ts | 154 +++++++++++++++++ tools/ui/src/lib/stores/ui.svelte.ts | 14 ++ tools/ui/src/lib/types/navigation.d.ts | 3 + tools/ui/src/routes/(chat)/+layout.svelte | 26 ++- tools/ui/src/routes/(chat)/+page.svelte | 18 +- tools/ui/src/routes/+layout.svelte | 43 ++++- tools/ui/src/routes/search/+page.svelte | 6 +- ...lte => ScrollCarousel.a11y.stories.svelte} | 15 +- 43 files changed, 1026 insertions(+), 323 deletions(-) create mode 100644 tools/ui/src/lib/components/app/chat/ChatTabs/ChatTabs.svelte create mode 100644 tools/ui/src/lib/components/app/chat/ChatTabs/ChatTabsItem.svelte create mode 100644 tools/ui/src/lib/components/app/chat/ChatTabs/ChatTabsNewChatButton.svelte delete mode 100644 tools/ui/src/lib/components/app/misc/HorizontalScrollCarousel.svelte create mode 100644 tools/ui/src/lib/components/app/misc/ScrollCarousel.svelte create mode 100644 tools/ui/src/lib/constants/chat-tabs.constants.ts create mode 100644 tools/ui/src/lib/stores/tabs.svelte.ts create mode 100644 tools/ui/src/lib/stores/ui.svelte.ts rename tools/ui/tests/stories/a11y/{HorizontalScrollCarousel.a11y.stories.svelte => ScrollCarousel.a11y.stories.svelte} (80%) diff --git a/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsList.svelte b/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsList.svelte index 36895c8e7..2de9460aa 100644 --- a/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsList.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsList.svelte @@ -3,8 +3,9 @@ ChatAttachmentsListItem, DialogChatAttachmentsPreview, DialogMcpResourcePreview, - HorizontalScrollCarousel + ScrollCarousel } from '$lib/components/app'; + import { ScrollCarouselVariant } from '$lib/enums'; import type { DatabaseMessageExtraMcpResource } from '$lib/types'; import { getAttachmentDisplayItems, isMcpPrompt, isMcpResource } from '$lib/utils'; @@ -42,7 +43,7 @@ uploadedFiles = $bindable([]) }: Props = $props(); - let carouselRef: HorizontalScrollCarousel | undefined = $state(); + let carouselRef: ScrollCarousel | undefined = $state(); let mcpResourcePreviewOpen = $state(false); let mcpResourcePreviewExtra = $state(null); let previewFocusIndex = $state(0); @@ -91,11 +92,11 @@ {#if displayItems.length > 0}
{#if limitToSingleRow} - + {#each displayItems as item (item.id)} {@render attachmentitem(item)} {/each} - + {:else}
{#each displayItems as item (item.id)} diff --git a/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsPreview/ChatAttachmentsPreviewThumbnailStrip.svelte b/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsPreview/ChatAttachmentsPreviewThumbnailStrip.svelte index 366c8372b..f0ea9675f 100644 --- a/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsPreview/ChatAttachmentsPreviewThumbnailStrip.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsPreview/ChatAttachmentsPreviewThumbnailStrip.svelte @@ -1,7 +1,8 @@ + + + + diff --git a/tools/ui/src/lib/components/app/chat/ChatTabs/ChatTabsItem.svelte b/tools/ui/src/lib/components/app/chat/ChatTabs/ChatTabsItem.svelte new file mode 100644 index 000000000..b76a88efe --- /dev/null +++ b/tools/ui/src/lib/components/app/chat/ChatTabs/ChatTabsItem.svelte @@ -0,0 +1,156 @@ + + + +
+ onAuxClick?.(tab.id, e)} + aria-current={isActive ? 'page' : undefined} + aria-label={tab.name} + > + + {#if isLoading} + + + {#snippet child({ props })} + + {/snippet} + + + +

Stop generation

+
+
+ {/if} + + {#if tab.isNewChat} + + {/if} + + {tab.name} + + + + {#snippet child({ props })} + + {/snippet} + + + +

Close tab

+
+
+
+ + diff --git a/tools/ui/src/lib/components/app/chat/ChatTabs/ChatTabsNewChatButton.svelte b/tools/ui/src/lib/components/app/chat/ChatTabs/ChatTabsNewChatButton.svelte new file mode 100644 index 000000000..d62e551f7 --- /dev/null +++ b/tools/ui/src/lib/components/app/chat/ChatTabs/ChatTabsNewChatButton.svelte @@ -0,0 +1,30 @@ + + + + + {#snippet child({ props })} + + {/snippet} + + + +

New chat

+
+
diff --git a/tools/ui/src/lib/components/app/chat/index.ts b/tools/ui/src/lib/components/app/chat/index.ts index 34571d53b..a96ae3789 100644 --- a/tools/ui/src/lib/components/app/chat/index.ts +++ b/tools/ui/src/lib/components/app/chat/index.ts @@ -686,6 +686,18 @@ export { default as ChatMessageSystem } from './ChatMessages/ChatMessage/ChatMes */ export { default as ChatScreen } from './ChatScreen/ChatScreen.svelte'; +/** + * **ChatTabs** - Browser-style tab bar for open conversations + * + * Horizontal strip of tabs rendered above ChatScreen in the chat layout, + * one per conversation tracked by tabsStore. The active tab follows the + * route's conversation id; clicking a tab navigates to it, middle-click or + * the close button closes it (switching to the left neighbor when closing + * the active tab), and a trailing "+" button starts a new chat. Shows a + * spinner on tabs with a running generation. Desktop-only. + */ +export { default as ChatTabs } from './ChatTabs/ChatTabs.svelte'; + /** * Visual overlay displayed when user drags files over the chat screen. * Shows drop zone indicator to guide users where to release files. diff --git a/tools/ui/src/lib/components/app/misc/HorizontalScrollCarousel.svelte b/tools/ui/src/lib/components/app/misc/HorizontalScrollCarousel.svelte deleted file mode 100644 index e2edb4d02..000000000 --- a/tools/ui/src/lib/components/app/misc/HorizontalScrollCarousel.svelte +++ /dev/null @@ -1,96 +0,0 @@ - - -
- - -
- {@render children?.()} -
- - -
diff --git a/tools/ui/src/lib/components/app/misc/ScrollCarousel.svelte b/tools/ui/src/lib/components/app/misc/ScrollCarousel.svelte new file mode 100644 index 000000000..a0a914f5e --- /dev/null +++ b/tools/ui/src/lib/components/app/misc/ScrollCarousel.svelte @@ -0,0 +1,131 @@ + + +
+ + +
+
+ {@render children?.()} +
+
+ + +
diff --git a/tools/ui/src/lib/components/app/misc/index.ts b/tools/ui/src/lib/components/app/misc/index.ts index b550ae66a..a10410ef9 100644 --- a/tools/ui/src/lib/components/app/misc/index.ts +++ b/tools/ui/src/lib/components/app/misc/index.ts @@ -21,13 +21,6 @@ */ export { default as ConversationSelection } from './ConversationSelection.svelte'; -/** - * Horizontal scrollable carousel with navigation arrows. - * Used for displaying items in a horizontally scrollable container - * with left/right navigation buttons that appear on hover. - */ -export { default as HorizontalScrollCarousel } from './HorizontalScrollCarousel.svelte'; - /** * **TruncatedText** - Text with ellipsis and tooltip * @@ -44,6 +37,13 @@ export { default as TruncatedText } from './TruncatedText.svelte'; */ export { default as KeyboardShortcutInfo } from './KeyboardShortcutInfo.svelte'; +/** + * **ScrollCarousel** - Feature/carousel with center-aligned overflow controls + * + * Horizontal scrollable container with arrows that center the focused item. + */ +export { default as ScrollCarousel } from './ScrollCarousel.svelte'; + /** * **CodeBlockActions** - Actions bar for code blocks (copy, preview) * diff --git a/tools/ui/src/lib/components/app/navigation/SidebarNavigation/SidebarNavigation.svelte b/tools/ui/src/lib/components/app/navigation/SidebarNavigation/SidebarNavigation.svelte index aa63c2915..424f2feca 100644 --- a/tools/ui/src/lib/components/app/navigation/SidebarNavigation/SidebarNavigation.svelte +++ b/tools/ui/src/lib/components/app/navigation/SidebarNavigation/SidebarNavigation.svelte @@ -14,7 +14,7 @@ import { useKeyboardShortcuts } from '$lib/hooks/use-keyboard-shortcuts.svelte'; import { useMarqueeSelection } from '$lib/hooks/use-marquee-selection.svelte'; import { RouterService } from '$lib/services/router.service'; - import { chatStore, conversationsStore, deviceStore, settingsStore } from '$lib/stores'; + import { chatStore, conversationsStore, deviceStore, settingsStore, uiStore } from '$lib/stores'; import { buildConversationTree } from '$lib/utils'; import { circIn } from 'svelte/easing'; import { SvelteSet } from 'svelte/reactivity'; @@ -31,30 +31,29 @@ toggleSidebar: () => toggleExpandedMode() }); - let isExpandedMode = $state(false); let hoveredTooltip = $state(null); let logoHovered = $state(false); - const isStripExpanded = $derived(isExpandedMode || hoveredTooltip !== null); + const isStripExpanded = $derived(uiStore.isSidebarExpanded || hoveredTooltip !== null); const isOnMobile = $derived(deviceStore.isMobile); const alwaysShowOnDesktop = $derived(settingsStore.config.alwaysShowSidebarOnDesktop as boolean); $effect(() => { if (alwaysShowOnDesktop && !isOnMobile) { - isExpandedMode = true; + uiStore.isSidebarExpanded = true; } }); function toggleExpandedMode() { - isExpandedMode = !isExpandedMode; + uiStore.isSidebarExpanded = !uiStore.isSidebarExpanded; - if (!isExpandedMode) { + if (!uiStore.isSidebarExpanded) { hoveredTooltip = null; } } $effect(() => { - if (!isExpandedMode) { + if (!uiStore.isSidebarExpanded) { isSearchModeActive = false; searchQuery = ''; @@ -66,7 +65,7 @@ $effect(() => { if (deviceStore.isMobile && page.url.hash.includes(ROUTES.SEARCH)) { - isExpandedMode = false; + uiStore.isSidebarExpanded = false; } }); @@ -294,7 +293,7 @@ } pendingCollapse = setTimeout(() => { - isExpandedMode = false; + uiStore.isSidebarExpanded = false; pendingCollapse = null; }, 100); } @@ -314,7 +313,7 @@ class={[ 'fixed md:sticky top-2 left-2 md:left-0 md:ml-2 md:mt-2 pt-2 z-10 w-[calc(100dvw-1rem)]', 'md:h-[calc(100dvh-1.125rem)]', - isExpandedMode && + uiStore.isSidebarExpanded && (deviceStore.isStandalone ? 'h-[calc(100dvh-2rem)]' : deviceStore.isIOSDevice @@ -323,9 +322,9 @@ 'rounded-3xl md:rounded-2xl', 'flex flex-col justify-between', 'md:transition-[width,padding] duration-200 ease-out', - isStripExpanded && 'md:w-72 md:bg-muted/60 md:backdrop-blur-xl border-border shadow-md', + isStripExpanded && 'md:w-72 md:bg-muted/60 md:backdrop-blur-xl shadow-md', !isStripExpanded && 'md:w-12', - isExpandedMode && 'is-expanded' + uiStore.isSidebarExpanded && 'is-expanded' ]} >
@@ -337,24 +336,26 @@ onmouseleave={() => (logoHovered = false)} > 768 ? PanelLeftOpen : Logo} + icon={!uiStore.isSidebarExpanded && logoHovered && innerWidth > 768 + ? PanelLeftOpen + : Logo} size="lg" iconSize="h-4.5 w-4.5 md:h-4 md:w-4" - class="{isExpandedMode + class="{uiStore.isSidebarExpanded ? 'bg-muted! md:bg-foreground/5!' : 'bg-transparent!'} md:h-9 md:w-9 h-10 w-10 rounded-full md:hover:bg-foreground/10! pointer-events-auto" - href={isExpandedMode ? ROUTES.START : undefined} - onclick={isExpandedMode ? undefined : toggleExpandedMode} - tooltip={isExpandedMode ? undefined : 'Open Sidebar'} + href={uiStore.isSidebarExpanded ? ROUTES.START : undefined} + onclick={uiStore.isSidebarExpanded ? undefined : toggleExpandedMode} + tooltip={uiStore.isSidebarExpanded ? undefined : 'Open Sidebar'} tooltipSide={TooltipSide.RIGHT} - ariaLabel={isExpandedMode ? 'Go to start' : 'Expand navigation'} + ariaLabel={uiStore.isSidebarExpanded ? 'Go to start' : 'Expand navigation'} />
- {#if isOnMobile || (isExpandedMode && !alwaysShowOnDesktop)} + {#if isOnMobile || (uiStore.isSidebarExpanded && !alwaysShowOnDesktop)}
768 ? isExpandedMode : true} + isExpandedMode={innerWidth > 768 ? uiStore.isSidebarExpanded : true} class="px-2" bind:isSearchModeActive bind:searchQuery @@ -391,7 +392,7 @@ searchQuery = ''; }} onSearchClick={() => { - isExpandedMode = true; + uiStore.isSidebarExpanded = true; isSearchModeActive = true; }} onNewChat={() => { @@ -401,7 +402,7 @@ }} /> - {#if isExpandedMode || isOnMobile} + {#if uiStore.isSidebarExpanded || isOnMobile}
{ - onNewChat?.(); - goto(item.route!); - } - : isSearchOnMobile - ? undefined - : onSearchClick} + {@const itemOnClick = + item.action === SidebarAction.NEW_CHAT + ? () => { + onNewChat?.(); + void conversationsStore.openNewChat(); + } + : item.route + ? () => { + onNewChat?.(); + goto(item.route!); + } + : isSearchOnMobile + ? undefined + : onSearchClick} {@const itemTransition = { delay: !initialized ? i * ICON_STRIP_TRANSITION_DELAY_MULTIPLIER : 0, duration: ICON_STRIP_TRANSITION_DURATION, @@ -157,14 +163,20 @@ {#each SIDEBAR_ACTIONS_ITEMS as item, i (item.tooltip)} {@const isActive = isItemActive(item)} {@const isSearchOnMobile = item.icon === Search && deviceStore.isMobile} - {@const itemOnClick = item.route - ? () => { - onNewChat?.(); - goto(item.route!); - } - : isSearchOnMobile - ? undefined - : onSearchClick} + {@const itemOnClick = + item.action === SidebarAction.NEW_CHAT + ? () => { + onNewChat?.(); + void conversationsStore.openNewChat(); + } + : item.route + ? () => { + onNewChat?.(); + goto(item.route!); + } + : isSearchOnMobile + ? undefined + : onSearchClick} {@const itemTransition = { delay: !initialized ? i * ICON_STRIP_TRANSITION_DELAY_MULTIPLIER : 0, duration: ICON_STRIP_TRANSITION_DURATION, diff --git a/tools/ui/src/lib/components/app/settings/SettingsChatMobileHeader.svelte b/tools/ui/src/lib/components/app/settings/SettingsChatMobileHeader.svelte index 58617956a..1e8c76f8e 100644 --- a/tools/ui/src/lib/components/app/settings/SettingsChatMobileHeader.svelte +++ b/tools/ui/src/lib/components/app/settings/SettingsChatMobileHeader.svelte @@ -1,5 +1,6 @@ - +
+ {#if showTabs} + + {/if} + + +
{@render children?.()} diff --git a/tools/ui/src/routes/(chat)/+page.svelte b/tools/ui/src/routes/(chat)/+page.svelte index de8574e35..95a3e1947 100644 --- a/tools/ui/src/routes/(chat)/+page.svelte +++ b/tools/ui/src/routes/(chat)/+page.svelte @@ -8,24 +8,19 @@ let qParam = $derived(page.url.searchParams.get(URL_PARAMS.QUERY)); let modelParam = $derived(page.url.searchParams.get(URL_PARAMS.MODEL)); - let newChatParam = $derived(page.url.searchParams.get(URL_PARAMS.NEW_CHAT)); let loadParam = $derived(page.url.searchParams.get(URL_PARAMS.LOAD)); - // Dialog state for model not available error let showModelNotAvailable = $state(false); let requestedModelName = $state(''); let availableModelNames = $derived(modelsStore.models.map((m) => m.model)); - /** - * Clear URL params after message is sent to prevent re-sending on refresh - */ + // Clear params after handling the deep link so a refresh does not replay them function clearUrlParams() { const url = new URL(page.url); url.searchParams.delete(URL_PARAMS.QUERY); url.searchParams.delete(URL_PARAMS.MODEL); url.searchParams.delete(URL_PARAMS.LOAD); - url.searchParams.delete(URL_PARAMS.NEW_CHAT); replaceState(url.toString(), {}); } @@ -40,8 +35,8 @@ try { await modelsStore.selectModelById(model.id); - // with ?load=true, start loading right away so the model is ready sooner; - // not awaited, so the UI stays usable during the load + // with ?load=true in router mode, start loading right away so the + // model is ready sooner; not awaited so the UI stays usable if ( loadParam === 'true' && serverStore.isRouterMode && @@ -66,11 +61,12 @@ } } - // Handle ?q= parameter - create new conversation and send message + // ?q= creates the conversation, the chat route sends the prompt once the + // conversation id is in the URL if (qParam !== null) { await conversationsStore.createConversation(); clearUrlParams(); - } else if (modelParam || newChatParam === 'true') { + } else if (modelParam) { clearUrlParams(); } } @@ -85,7 +81,7 @@ await modelsStore.fetch(); - if (qParam !== null || modelParam !== null || newChatParam === 'true') { + if (qParam !== null || modelParam !== null) { await handleUrlParams(); } diff --git a/tools/ui/src/routes/+layout.svelte b/tools/ui/src/routes/+layout.svelte index f87bbe26a..16adfe332 100644 --- a/tools/ui/src/routes/+layout.svelte +++ b/tools/ui/src/routes/+layout.svelte @@ -11,6 +11,7 @@ FAVICON_PATHS, FAVICON_SELECTORS, HEADERS, + NEW_CHAT_TAB_ID, ROUTES, SETTINGS_KEYS, TOOLTIP_DELAY_DURATION @@ -26,6 +27,7 @@ modelsStore, serverStore, settingsStore, + tabsStore, versionStore } from '$lib/stores'; import { initStores } from '$lib/stores/init'; @@ -74,6 +76,27 @@ } } + function navigateToTab(direction: -1 | 1) { + // only makes sense with conversation tabs enabled + if (!settingsStore.config.conversationTabs) return; + + const openTabs = tabsStore.openTabs; + + if (openTabs.length === 0) return; + + const activeId = page.params.id ?? NEW_CHAT_TAB_ID; + const idx = openTabs.indexOf(activeId); + // active tab not in list (e.g. a non-chat route): start from an edge + const targetIdx = + idx === -1 + ? direction === 1 + ? 0 + : openTabs.length - 1 + : (idx + direction + openTabs.length) % openTabs.length; + + void tabsStore.activate(openTabs[targetIdx]); + } + function navigateToConversation(direction: -1 | 1) { const allConvs = conversationsStore.conversations; @@ -96,15 +119,31 @@ if (targetIdx >= 0 && targetIdx < allConvs.length) { goto(RouterService.chat(allConvs[targetIdx].id)); } else { - goto(ROUTES.NEW_CHAT); + conversationsStore.openNewChat(); } } + // navigating away from the new-chat screen drops its tab, so it does not + // linger once the user moves to a real conversation or another route + let previousChatId = $state(undefined); + + $effect(() => { + const id = page.params.id ?? (page.route.id === '/(chat)' ? NEW_CHAT_TAB_ID : undefined); + const prev = untrack(() => previousChatId); + + previousChatId = id; + + if (id !== prev && prev && settingsStore.config.conversationTabs && prev === NEW_CHAT_TAB_ID) { + untrack(() => tabsStore.removeTabs([NEW_CHAT_TAB_ID])); + } + }); // Global keyboard shortcuts const { handleKeydown } = useKeyboardShortcuts({ editActiveConversation: () => chatSidebar?.editActiveConversation?.(), navigateToNextConversation: () => navigateToConversation(1), - navigateToPrevConversation: () => navigateToConversation(-1) + navigateToNextTab: () => navigateToTab(1), + navigateToPrevConversation: () => navigateToConversation(-1), + navigateToPrevTab: () => navigateToTab(-1) }); function checkApiKey() { diff --git a/tools/ui/src/routes/search/+page.svelte b/tools/ui/src/routes/search/+page.svelte index a882e5552..548b9911a 100644 --- a/tools/ui/src/routes/search/+page.svelte +++ b/tools/ui/src/routes/search/+page.svelte @@ -21,10 +21,10 @@ }); // Search page is intended for mobile; on desktop the sidebar already exposes - // in-place search, so bounce back to a chat. + // in-place search, so bounce back to a new-chat tab without a history entry. $effect(() => { if (browser && !deviceStore.isMobile) { - goto(ROUTES.NEW_CHAT, { replaceState: true }); + goto(ROUTES.START, { replaceState: true }); } }); @@ -66,7 +66,7 @@ if (history.length > 1) { history.back(); } else { - goto(ROUTES.NEW_CHAT); + conversationsStore.openNewChat(); } } diff --git a/tools/ui/tests/stories/a11y/HorizontalScrollCarousel.a11y.stories.svelte b/tools/ui/tests/stories/a11y/ScrollCarousel.a11y.stories.svelte similarity index 80% rename from tools/ui/tests/stories/a11y/HorizontalScrollCarousel.a11y.stories.svelte rename to tools/ui/tests/stories/a11y/ScrollCarousel.a11y.stories.svelte index ef5abeafa..b9bf5afbc 100644 --- a/tools/ui/tests/stories/a11y/HorizontalScrollCarousel.a11y.stories.svelte +++ b/tools/ui/tests/stories/a11y/ScrollCarousel.a11y.stories.svelte @@ -1,15 +1,16 @@ @@ -33,10 +34,10 @@ >
- +
-
+
@@ -60,10 +61,10 @@ >
- + {#each [...Array(20).keys()] as i (i)}
{i}
{/each} -
+
From d3371929bb1b6982cf73f1e54156d3d426cd80a1 Mon Sep 17 00:00:00 2001 From: Gaurav Garg Date: Sun, 23 Aug 2026 16:19:12 +0530 Subject: [PATCH 08/28] [Tensor parallel] Fix meta tensor split state propagation (#27574) * ggml : fix meta tensor split state propagation * Add test-llama-archs to CI --- ci/run.sh | 29 +++++++++++++++ ggml/src/ggml-backend-meta.cpp | 68 ++++++++++++++++++++++++---------- src/llama-model.cpp | 28 ++++++++++++-- tests/test-llama-archs.cpp | 12 +++++- 4 files changed, 113 insertions(+), 24 deletions(-) diff --git a/ci/run.sh b/ci/run.sh index 3d1d75b5b..02ce54916 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -300,6 +300,31 @@ function gg_sum_ctest_release { gg_printf '```\n' } +# test_llama_archs_tensor_split + +function gg_run_test_llama_archs_tensor_split { + cd ${SRC} + + set -e + + GGML_CUDA_DEVICES=1 ./build-ci-release/bin/test-llama-archs -s 1 2>&1 + GGML_CUDA_DEVICES=2 ./build-ci-release/bin/test-llama-archs -s 1 2>&1 + GGML_CUDA_DEVICES=3 ./build-ci-release/bin/test-llama-archs -s 1 2>&1 + GGML_CUDA_DEVICES=4 ./build-ci-release/bin/test-llama-archs -s 1 2>&1 + + set +e +} + +function gg_sum_test_llama_archs_tensor_split { + gg_printf '### %s\n\n' "${ci}" + + gg_printf 'Runs test-llama-archs with 1 to 4 CUDA devices\n' + gg_printf '- status: %s\n' "$(cat $OUT/${ci}.exit)" + gg_printf '```\n' + gg_printf '%s\n' "$(cat $OUT/${ci}.log)" + gg_printf '```\n' +} + # test_scripts function gg_run_test_scripts { @@ -751,6 +776,10 @@ ret=0 test $ret -eq 0 && gg_run ctest_debug test $ret -eq 0 && gg_run ctest_release +if [ ! -z ${GG_BUILD_CUDA} ]; then + test $ret -eq 0 && gg_run test_llama_archs_tensor_split +fi + if [ ! -z ${GG_BUILD_HIGH_PERF} ]; then test $ret -eq 0 && gg_run test_backend_ops_cpu fi diff --git a/ggml/src/ggml-backend-meta.cpp b/ggml/src/ggml-backend-meta.cpp index 7654ea1f3..ded678e68 100644 --- a/ggml/src/ggml-backend-meta.cpp +++ b/ggml/src/ggml-backend-meta.cpp @@ -602,27 +602,40 @@ static struct ggml_backend_meta_split_state ggml_backend_meta_get_split_state( case GGML_BACKEND_SPLIT_AXIS_1: case GGML_BACKEND_SPLIT_AXIS_2: case GGML_BACKEND_SPLIT_AXIS_3: { - GGML_ASSERT(src_ss[0].n_segments == 1); - if (src_ss[0].axis == ggml_n_dims(tensor->src[0]) - 1 && src_ss[0].nr[0] == 1) { - return {ggml_backend_meta_split_axis(ggml_n_dims(tensor) - 1), {0}, {1}, 1}; - } - int64_t base_ne_in = tensor->src[0]->ne[0]; - for (int dim = 1; dim <= src_ss[0].axis; dim++) { + int64_t base_ne_in = 1; + for (int dim = 0; dim <= src_ss[0].axis; dim++) { base_ne_in *= tensor->src[0]->ne[dim]; } - base_ne_in /= src_ss[0].nr[0]; + if (src_ss[0].n_segments == 1) { + base_ne_in /= src_ss[0].nr[0]; + if (src_ss[0].axis == ggml_n_dims(tensor->src[0]) - 1 && src_ss[0].nr[0] == 1) { + return {ggml_backend_meta_split_axis(ggml_n_dims(tensor) - 1), {0}, {1}, 1}; + } + if (src_ss[0].axis == GGML_BACKEND_SPLIT_AXIS_0 && tensor->ne[0] == tensor->src[0]->ne[0] && + tensor->ne[1] == 1 && src_ss[0].nr[0] == 1) { + bool complete_rows = true; + for (size_t j = 0; j < n_bufs; j++) { + const int64_t ne = src_ss[0].ne[j]; + complete_rows = complete_rows && (ne == 0 || ne == tensor->src[0]->ne[0]); + } + if (complete_rows) { + // Move a complete dim-0 split to the following singleton dimension. + return {GGML_BACKEND_SPLIT_AXIS_1, {0}, {1}, 1}; + } + } + } + // Reshape outputs use one segment; split-state propagation merges source segments. int64_t base_ne_out = 1; for (int dim = 0; dim < GGML_MAX_DIMS; dim++) { - const int64_t base_ne_out_next = base_ne_out *= tensor->ne[dim]; - if (base_ne_out_next % base_ne_in == 0) { - return {ggml_backend_meta_split_axis(dim), {0}, {uint32_t(base_ne_out_next/base_ne_in)}, 1}; + base_ne_out *= tensor->ne[dim]; + if (base_ne_out % base_ne_in == 0) { + return {ggml_backend_meta_split_axis(dim), {0}, {uint32_t(base_ne_out/base_ne_in)}, 1}; } - if (base_ne_out_next > base_ne_in) { + if (base_ne_out > base_ne_in) { GGML_ASSERT(src_ss[0].n_segments == 1); GGML_ASSERT(src_ss[0].nr[0] == 1); return {ggml_backend_meta_split_axis(dim), {0}, {1}, 1}; } - base_ne_out = base_ne_out_next; } GGML_ABORT("shape mismatch for %s", ggml_op_name(tensor->op)); } @@ -792,7 +805,7 @@ static struct ggml_backend_meta_split_state ggml_backend_meta_get_split_state( ggml_backend_dev_t dev = ggml_backend_buft_get_device(ggml_backend_buffer_get_type(tensor->buffer)); const ggml_backend_meta_device_context * dev_ctx = (const ggml_backend_meta_device_context *) dev->context; ggml_backend_meta_split_state ret = dev_ctx->get_split_state(tensor, dev_ctx->get_split_state_ud); - if (ret.axis >= 0 && ret.axis <= GGML_MAX_DIMS) { + if (ret.axis >= 0 && ret.axis < GGML_MAX_DIMS) { const int64_t granularity = ret.axis == GGML_BACKEND_SPLIT_AXIS_0 ? ggml_blck_size(tensor->type) : 1; int64_t ne_sum = 0; for (size_t s = 0; s < ret.n_segments; s++) { @@ -802,6 +815,9 @@ static struct ggml_backend_meta_split_state ggml_backend_meta_get_split_state( } } GGML_ASSERT(ne_sum == tensor->ne[ret.axis]); + } else if (ret.axis == GGML_BACKEND_SPLIT_AXIS_PARTIAL) { + GGML_ASSERT(ret.n_segments == 1); + GGML_ASSERT(ret.nr[0] == 1); } return ret; } @@ -1352,15 +1368,29 @@ static void ggml_backend_meta_buffer_set_tensor(ggml_backend_buffer_t buffer, gg } break; case GGML_BACKEND_SPLIT_AXIS_PARTIAL: { GGML_ASSERT(tensor->type == GGML_TYPE_F32); - const int64_t ne = ggml_nelements(tensor); - std::vector tmp; - tmp.reserve(ne); - for (int64_t i = 0; i < ne; i++) { - tmp.push_back(((const float *) data)[i] / n_bufs); + GGML_ASSERT(offset % sizeof(float) == 0); + GGML_ASSERT(size % sizeof(float) == 0); + const size_t n_values = size / sizeof(float); + size_t n_contributors = 0; + for (size_t j = 0; j < n_bufs; j++) { + n_contributors += split_state.ne[j] != 0; + } + const bool has_contributor_mask = n_contributors != 0; + if (!has_contributor_mask) { + n_contributors = n_bufs; + } + std::vector tmp(n_values); + for (size_t i = 0; i < n_values; i++) { + tmp[i] = ((const float *) data)[i] / n_contributors; + } + std::vector zero; + if (has_contributor_mask) { + zero.resize(n_values, 0.0f); } for (size_t j = 0; j < n_bufs; j++) { ggml_tensor * simple_tensor = ggml_backend_meta_buffer_simple_tensor(tensor, j); - ggml_backend_tensor_set(simple_tensor, tmp.data(), offset, size); + const float * partial = has_contributor_mask && split_state.ne[j] == 0 ? zero.data() : tmp.data(); + ggml_backend_tensor_set(simple_tensor, partial, offset, size); } } break; default: { diff --git a/src/llama-model.cpp b/src/llama-model.cpp index de0d3c1a6..33f5661b2 100644 --- a/src/llama-model.cpp +++ b/src/llama-model.cpp @@ -520,7 +520,7 @@ struct ggml_backend_meta_split_state llama_meta_device_get_split_state(const str return get_tensor_config_impl(GGML_BACKEND_SPLIT_AXIS_MIRRORED); } if (std::regex_match(tensor_name, pattern_ffn_down_exps_bias)) { - return get_tensor_config_impl(GGML_BACKEND_SPLIT_AXIS_PARTIAL); + return get_tensor_config_impl(GGML_BACKEND_SPLIT_AXIS_PARTIAL, "ffn_down_exps.weight"); } // output @@ -554,6 +554,9 @@ struct ggml_backend_meta_split_state llama_meta_device_get_split_state(const str GGML_ASSERT(tensor->ne[axis] == 2*key_dim + value_dim); return {{key_dim, 2}, {value_dim, 1}}; } + if (std::regex_match(tensor_name, pattern_r_cache)) { + return {{key_dim * (hparams.ssm_d_conv - 1), 2}, {value_dim * (hparams.ssm_d_conv - 1), 1}}; + } } else { const int64_t head_ratio = n_v_heads / n_k_heads; if (std::regex_match(tensor_name, pattern_qkv_weight) || std::regex_match(tensor_name, pattern_ssm_conv1d)) { @@ -642,12 +645,12 @@ struct ggml_backend_meta_split_state llama_meta_device_get_split_state(const str blck_size_perf *= 2; } + const int64_t granularity_q = std::lcm(n_embd_q, blck_size_perf); + const int64_t granularity_head = granularity_q / hparams.n_embd_head_k(il); // for tensors with one value per head if (std::regex_match(tensor_name, pattern_attn_sinks)) { GGML_ASSERT(segments.size() == 1); - return {std::lcm(n_embd_q, blck_size_perf)/n_embd_q * n_gqa}; + return {granularity_head}; } - - const int64_t granularity_q = std::lcm(n_embd_q, blck_size_perf); if (std::regex_match(tensor_name, pattern_q_weight) || std::regex_match(tensor_name, pattern_q_bias)) { GGML_ASSERT(segments.size() == 1); // some models have Q gate tensors, for those cases the granularity needs to be doubled: @@ -660,6 +663,13 @@ struct ggml_backend_meta_split_state llama_meta_device_get_split_state(const str GGML_ASSERT(segments.size() == 1); return {granularity_q}; } + if (std::regex_match(tensor_name, pattern_attn_gate_weight)) { + GGML_ASSERT(segments.size() == 1); + if (tensor->ne[1] == hparams.n_head(il)) { + return {granularity_head}; + } + return {granularity_q}; + } const int64_t granularity_kv = granularity_q / n_gqa; if (std::regex_match(tensor_name, pattern_kv_weight) || @@ -728,6 +738,16 @@ struct ggml_backend_meta_split_state llama_meta_device_get_split_state(const str memset(split_state.ne, 0, sizeof(split_state.ne)); split_state.nr[0] = 1; split_state.n_segments = 1; + if (split_state.axis == GGML_BACKEND_SPLIT_AXIS_PARTIAL) { + GGML_ASSERT(tc.tensor_axis_0 != tensor); + const ggml_backend_meta_split_state source_split_state = llama_meta_device_get_split_state(tc.tensor_axis_0, userdata); + GGML_ASSERT(source_split_state.axis >= 0 && source_split_state.axis < GGML_MAX_DIMS); + for (size_t j = 0; j < ud->n_devices; j++) { + for (size_t is = 0; is < source_split_state.n_segments; is++) { + split_state.ne[j] += source_split_state.ne[is*ud->n_devices + j] * source_split_state.nr[is]; + } + } + } } return split_state; GGML_UNUSED(userdata); diff --git a/tests/test-llama-archs.cpp b/tests/test-llama-archs.cpp index 07e3a7a11..18676f2be 100644 --- a/tests/test-llama-archs.cpp +++ b/tests/test-llama-archs.cpp @@ -101,6 +101,10 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) { n_head = 1; n_ff = 96; n_layer = 22; // hparams.n_layer_kv_from_start = 20 is hardcoded + } else if (arch == LLM_ARCH_STEP35 || arch == LLM_ARCH_LAGUNA) { + n_embd = 160; // exercise per-head tensor split granularity with head size 80 + } else if (arch == LLM_ARCH_QWEN3 || arch == LLM_ARCH_MUSE_GLIMMER || arch == LLM_ARCH_AFMOE) { + n_head = 4; } else if (arch == LLM_ARCH_DEEPSEEK2 || arch == LLM_ARCH_DEEPSEEK32 || arch == LLM_ARCH_GLM_DSA @@ -120,6 +124,12 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) { n_vocab = 4096; // must be >= the hard-coded codec head size (3072) } + uint32_t n_head_kv = n_head; + if (arch == LLM_ARCH_QWEN3) { + n_head_kv = 1; // MQA coverage + } else if (arch == LLM_ARCH_MUSE_GLIMMER || arch == LLM_ARCH_AFMOE) { + n_head_kv = 2; // GQA coverage + } const uint32_t n_embd_head = n_embd / n_head; ms.add_kv(LLM_KV_GENERAL_ARCHITECTURE, llm_arch_name(arch)); @@ -160,7 +170,7 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) { ms.add_kv(LLM_KV_ATTENTION_HEAD_COUNT_KV, n_head_per_layer); } else { ms.add_kv(LLM_KV_ATTENTION_HEAD_COUNT, n_head); - ms.add_kv(LLM_KV_ATTENTION_HEAD_COUNT_KV, n_head); + ms.add_kv(LLM_KV_ATTENTION_HEAD_COUNT_KV, n_head_kv); } ms.add_kv(LLM_KV_ATTENTION_MAX_ALIBI_BIAS, 8.0f); From b0539c43ed13b16bf0d8a0840646faea65469702 Mon Sep 17 00:00:00 2001 From: Aman Gupta Date: Sun, 23 Aug 2026 16:27:49 +0530 Subject: [PATCH 09/28] DeepseekV4: fix rollback with multi-seq (#26756) * DeepseekV4: fix rollback with multi-seq * fix model loading * make pending rollback single use * only clear cache for seq_id for full load * add assert for compress ratio * make graph topology static * pass true instead of flags in clear_compressed * cont : clean-up + TODOs --------- Co-authored-by: Georgi Gerganov --- include/llama.h | 2 +- src/llama-context.cpp | 4 - src/llama-kv-cache-dsv4.cpp | 137 ++++++++++++------ src/llama-kv-cache.cpp | 2 + src/llama-memory-recurrent.cpp | 34 +++-- src/llama-model-saver.cpp | 11 ++ src/models/deepseek4.cpp | 2 + tests/CMakeLists.txt | 9 ++ tests/test-llama-archs.cpp | 29 +++- tests/test-recurrent-state-rollback.cpp | 177 ++++++++++++++++++++++++ 10 files changed, 345 insertions(+), 62 deletions(-) diff --git a/include/llama.h b/include/llama.h index 177fc10a9..a04177f9f 100644 --- a/include/llama.h +++ b/include/llama.h @@ -733,7 +733,7 @@ extern "C" { // Removes all tokens that belong to the specified sequence and have positions in [p0, p1) // Returns false if a partial sequence cannot be removed. Removing a whole sequence never fails - // seq_id < 0 : match any sequence + // seq_id < 0 : match any sequence [TAG_LLAMA_SEQ_ID_NEG] // p0 < 0 : [0, p1] // p1 < 0 : [p0, inf) LLAMA_API bool llama_memory_seq_rm( diff --git a/src/llama-context.cpp b/src/llama-context.cpp index 52f8d5367..0402044da 100644 --- a/src/llama-context.cpp +++ b/src/llama-context.cpp @@ -3218,8 +3218,6 @@ size_t llama_context::state_read_data(llama_io_read_i & io) { } size_t llama_context::state_seq_write_data(llama_io_write_i & io, llama_seq_id seq_id, llama_state_seq_flags flags) { - GGML_UNUSED(seq_id); - if (memory) { memory->state_write(io, seq_id, flags); } @@ -3228,8 +3226,6 @@ size_t llama_context::state_seq_write_data(llama_io_write_i & io, llama_seq_id s } size_t llama_context::state_seq_read_data(llama_io_read_i & io, llama_seq_id seq_id, llama_state_seq_flags flags) { - GGML_UNUSED(seq_id); - if (memory) { memory->state_read(io, seq_id, flags); } diff --git a/src/llama-kv-cache-dsv4.cpp b/src/llama-kv-cache-dsv4.cpp index 5caa05e8b..58f78e438 100644 --- a/src/llama-kv-cache-dsv4.cpp +++ b/src/llama-kv-cache-dsv4.cpp @@ -599,6 +599,33 @@ static llama_kv_cache_dsv4_context::comp_plan dsv4_build_comp_plan( } } + if (ratio == DSV4_HCA_RATIO && !plan.state_pos.empty() && plan.state_write_idxs.empty()) { + assert(kv_size > 0); + // the last slot must not be live, or the dummy write would corrupt it; + // a full stream implies a completed block, which implies real writes + assert(plan.n_kv < (int64_t) kv_size); + + // Keep the compress/write ops in the graph when no HCA block completes + // in this ubatch. The dummy block writes to the last cache slot and is + // masked out. + uint32_t i = 0; + while (i < ubatch.n_tokens && ubatch.pos[i] < 0) { + ++i; + } + assert(i < ubatch.n_tokens); + + const llama_seq_id seq_id = ubatch.seq_id[i][0]; + const int64_t cache_off = dsv4_stream_offset(n_stream, seq_id, kv_size); + const int32_t source_idx = state_source_idx(seq_id, ubatch.pos[i]); + + plan.state_write_idxs.push_back(cache_off + kv_size - 1); + plan.state_write_pos .push_back(0); + + for (uint32_t j = 0; j < ratio; ++j) { + plan.state_read_idxs.push_back(source_idx); + } + } + if (overlap) { // [ all blocks' prev-window indices | all blocks' cur-window indices ] plan.state_read_idxs.reserve(overlap_prev_reads.size() + overlap_cur_reads.size()); @@ -608,7 +635,10 @@ static llama_kv_cache_dsv4_context::comp_plan dsv4_build_comp_plan( overlap_cur_reads.begin(), overlap_cur_reads.end()); } - plan.n_kv = GGML_PAD(plan.n_kv, 256u); + // Keep the mask (and with it the compressed-attention branch) present even + // before the first block is visible, so the graph topology never changes. + // Padded slots are masked out; comp cache buffers are zero-initialized. + plan.n_kv = std::max(GGML_PAD(plan.n_kv, 256u), 256); std::sort(persist_rows.begin(), persist_rows.end(), [](const persist_row & a, const persist_row & b) { @@ -620,16 +650,26 @@ static llama_kv_cache_dsv4_context::comp_plan dsv4_build_comp_plan( plan.state_persist_dst_idxs.push_back(row.dst); } - if (n_rs_seq > 0) { - for (uint32_t s = 0; s < ubatch.n_seqs_unq; ++s) { - const llama_seq_id seq_id = ubatch.seq_id_unq[s]; - if (seq_id < 0 || (uint32_t) seq_id >= n_stream) { - continue; + // Emit restore/snapshot entries for all layout streams so that the + // graph tensor sizes do not depend on the ubatch's sequence count. + // Streams not present in the ubatch get no-op entries. + for (uint32_t stream = 0; stream < n_stream; ++stream) { + llama_seq_id seq_id = -1; + if (n_stream == 1) { + // a unified stream serves any single sequence + seq_id = ubatch.n_seqs_unq > 0 ? ubatch.seq_id_unq[0] : -1; + } else { + for (uint32_t s = 0; s < ubatch.n_seqs_unq; ++s) { + if (ubatch.seq_id_unq[s] == (llama_seq_id) stream) { + seq_id = ubatch.seq_id_unq[s]; + break; + } + } } - const int64_t stream_off = dsv4_stream_offset(n_stream, seq_id, state_size); - const uint32_t rollback = (uint32_t) seq_id < rs_idx.size() ? rs_idx[seq_id] : 0; + const int64_t stream_off = (int64_t) stream*state_size; + const uint32_t rollback = seq_id >= 0 && (uint32_t) seq_id < rs_idx.size() ? rs_idx[seq_id] : 0; // Keep the restore graph fixed-width when no rollback is pending. const int64_t src_plane = rollback > 0 && rollback <= n_rs_seq ? (int64_t) rollback*state_rows : 0; for (uint32_t r = 0; r < state_size; ++r) { @@ -639,35 +679,33 @@ static llama_kv_cache_dsv4_context::comp_plan dsv4_build_comp_plan( std::vector token_idxs; token_idxs.reserve(ubatch.n_tokens); - for (uint32_t i = 0; i < ubatch.n_tokens; ++i) { - if (dsv4_token_has_seq(ubatch, i, seq_id)) { - token_idxs.push_back(i); + if (seq_id >= 0) { + for (uint32_t i = 0; i < ubatch.n_tokens; ++i) { + if (dsv4_token_has_seq(ubatch, i, seq_id)) { + token_idxs.push_back(i); + } } } - if (token_idxs.empty()) { - continue; - } const uint32_t n_seq_tokens = (uint32_t) token_idxs.size(); const int64_t scratch_off = (int64_t) state_rows*(1 + n_rs_seq); for (uint32_t d = 1; d <= n_rs_seq; ++d) { const int64_t dst_plane = (int64_t) d*state_rows; + const uint32_t prefix = d <= n_seq_tokens ? n_seq_tokens - d : 0; for (uint32_t r = 0; r < state_size; ++r) { - int32_t src; - if (d <= n_seq_tokens) { - const uint32_t prefix = n_seq_tokens - d; - src = (int32_t) (stream_off + r); + int32_t src = (int32_t) (stream_off + r); - for (uint32_t j = 0; j < prefix; ++j) { - const uint32_t i_tok = token_idxs[j]; - if (ubatch.pos[i_tok] >= 0 && (uint32_t) (ubatch.pos[i_tok]%state_size) == r) { - src = (int32_t) (scratch_off + i_tok); - } + for (uint32_t j = 0; j < prefix; ++j) { + const uint32_t i_tok = token_idxs[j]; + if (ubatch.pos[i_tok] >= 0 && (uint32_t) (ubatch.pos[i_tok]%state_size) == r) { + src = (int32_t) (scratch_off + i_tok); } - } else { - const int64_t src_plane = (int64_t) (d - n_seq_tokens)*state_rows; - src = (int32_t) (src_plane + stream_off + r); + } + + if (n_seq_tokens == 0) { + // no-op: copy the snapshot plane onto itself + src = (int32_t) (dst_plane + stream_off + r); } plan.state_snapshot_src_idxs.push_back(src); @@ -683,10 +721,16 @@ static llama_kv_cache_dsv4_context::comp_plan dsv4_build_comp_plan( }(); if (debug) { - LLAMA_LOG_INFO("%s: ratio=%u, n_tokens=%u, state_persist_dst=%s, state_write_pos=%s\n", - __func__, ratio, ubatch.n_tokens, + LLAMA_LOG_DEBUG("%s: ratio=%u, n_tokens=%u, n_seqs_unq=%u, state_persist_dst=%s, state_write_pos=%s\n", + __func__, ratio, ubatch.n_tokens, ubatch.n_seqs_unq, dsv4_plan_positions(plan.state_persist_dst_idxs).c_str(), dsv4_plan_positions(plan.state_write_pos).c_str()); + for (uint32_t s = 0; s < ubatch.n_seqs_unq; ++s) { + const llama_seq_id seq_id = ubatch.seq_id_unq[s]; + const uint32_t rollback = seq_id >= 0 && (uint32_t) seq_id < rs_idx.size() ? rs_idx[seq_id] : 0; + LLAMA_LOG_DEBUG("%s: seq %d pos [%d, %d] rollback=%u\n", __func__, seq_id, + ubatch.pos[0], ubatch.pos[ubatch.n_tokens - 1], rollback); + } } return plan; @@ -704,8 +748,17 @@ static std::vector dsv4_build_comp_plans std::vector plans; plans.reserve(ubatches.size()); + // the first ubatch touching a seq consumes its rollback restore + std::vector rs(rs_idx); for (const llama_ubatch & ubatch : ubatches) { - plans.push_back(dsv4_build_comp_plan(ubatch, ratio, overlap, state_size, kv_size, n_stream, n_rs_seq, rs_idx)); + plans.push_back(dsv4_build_comp_plan(ubatch, ratio, overlap, state_size, kv_size, n_stream, n_rs_seq, rs)); + + for (uint32_t s = 0; s < ubatch.n_seqs_unq; ++s) { + const llama_seq_id seq_id = ubatch.seq_id_unq[s]; + if (seq_id >= 0 && (size_t) seq_id < rs.size()) { + rs[seq_id] = 0; + } + } } return plans; @@ -803,16 +856,15 @@ static llama_kv_cache_dsv4_context::comp_plan dsv4_build_reserve_comp_plan( return plan; } - const uint32_t n_seqs = std::max(1, ubatch.n_seqs); - const uint32_t n_seq_tokens = std::max(1, ubatch.n_seq_tokens); - const uint64_t n_blocks_u64 = (uint64_t) n_seqs*((n_seq_tokens + ratio - 1)/ratio); - const size_t n_blocks = (size_t) std::max(1, n_blocks_u64); - GGML_ASSERT((uint64_t) n_blocks == std::max(1, n_blocks_u64)); + // worst case over every seq split: sum of per-seq ceil(tokens/ratio) is at + // most floor(n_tokens/ratio) + n_seqs + const uint32_t n_seqs = std::max(1, ubatch.n_seqs); + const size_t n_blocks = (size_t) ubatch.n_tokens/ratio + n_seqs; const uint64_t state_rows = (uint64_t) state_size*n_stream; const size_t n_persist = (size_t) std::min(ubatch.n_tokens, state_rows); - const size_t n_restore = n_rs_seq > 0 ? (size_t) state_size*std::max(1, ubatch.n_seqs_unq) : 0; - const size_t n_snapshot = (size_t) n_rs_seq*state_size*std::max(1, ubatch.n_seqs_unq); + const size_t n_restore = n_rs_seq > 0 ? (size_t) state_size*n_stream : 0; + const size_t n_snapshot = (size_t) n_rs_seq*state_size*n_stream; plan.state_pos .resize(ubatch.n_tokens); plan.state_persist_src_idxs.resize(n_persist); @@ -1356,7 +1408,9 @@ llama_memory_context_ptr llama_kv_cache_dsv4::init_batch( if (has_coupled) { ubatch = balloc.split_seq(n_ubatch); } else { - ubatch = balloc.split_equal(n_ubatch, raw_per_seq || comp_per_seq, 0); + // [TAG_RECURRENT_ROLLBACK_SPLITS] + // the trailing (1 + n_rs_seq) tokens of each seq must stay in the same ubatch + ubatch = balloc.split_equal(n_ubatch, raw_per_seq || comp_per_seq, n_rs_seq > 0 ? n_rs_seq + 1 : 0); } if (ubatch.n_tokens == 0) { @@ -1433,6 +1487,11 @@ bool llama_kv_cache_dsv4::seq_rm(llama_seq_id seq_id, llama_pos p0, llama_pos p1 return false; } + // pending rollback is single-use: stacked partial removals don't compose + if (rs_idx[seq_id] != 0) { + return false; + } + const bool res = kv_raw->seq_rm(seq_id, p0, p1); if (res) { rs_idx[seq_id] = (uint32_t) rollback; @@ -1594,9 +1653,7 @@ void llama_kv_cache_dsv4::state_read(llama_io_read_i & io, llama_seq_id seq_id, kv_raw->state_read(io, seq_id, flags); if (!partial_only) { - kv_csa->clear(true); - kv_hca->clear(true); - kv_lid->clear(true); + clear_compressed(seq_id, true); dsv4_state_read_k_cache(io, kv_csa.get(), seq_id, flags); dsv4_state_read_k_cache(io, kv_hca.get(), seq_id, flags); diff --git a/src/llama-kv-cache.cpp b/src/llama-kv-cache.cpp index 2e2bd7dc6..ec0f5a753 100644 --- a/src/llama-kv-cache.cpp +++ b/src/llama-kv-cache.cpp @@ -383,6 +383,7 @@ bool llama_kv_cache::seq_rm(llama_seq_id seq_id, llama_pos p0, llama_pos p1) { return true; } + // TODO: fix incosistent handling of `seq_id < 0` and `seq_id == -1` in the codebase [TAG_LLAMA_SEQ_ID_NEG] GGML_ASSERT(seq_id == -1 || (seq_id >= 0 && (size_t) seq_id < seq_to_stream.size())); if (p0 < 0) { @@ -2043,6 +2044,7 @@ void llama_kv_cache::state_read(llama_io_read_i & io, llama_seq_id seq_id, llama GGML_UNUSED(flags); + // TODO: fix incosistent handling of `seq_id < 0` and `seq_id == -1` in the codebase [TAG_LLAMA_SEQ_ID_NEG] GGML_ASSERT(seq_id == -1 || (seq_id >= 0 && (size_t) seq_id < seq_to_stream.size())); uint32_t n_stream_cur; diff --git a/src/llama-memory-recurrent.cpp b/src/llama-memory-recurrent.cpp index ef82eb976..e2990972e 100644 --- a/src/llama-memory-recurrent.cpp +++ b/src/llama-memory-recurrent.cpp @@ -158,13 +158,14 @@ bool llama_memory_recurrent::seq_rm(llama_seq_id seq_id, llama_pos p0, llama_pos p1 = std::numeric_limits::max(); } + if ((uint32_t) seq_id >= this->n_seq_max) { + LLAMA_LOG_ERROR("%s: invalid seq_id (%d) - larger than n_seq_max (%d)\n", __func__, seq_id, this->n_seq_max); + return false; + } + const bool rm_all = p0 == 0 && p1 == std::numeric_limits::max(); if (rm_all) { - if (seq_id >= 0) { - set_rs_idx(seq_id, 0); - } else { - std::fill(rs_idx.begin(), rs_idx.end(), 0); - } + set_rs_idx(seq_id, 0); } // models like Mamba or RWKV can't have a state partially erased at the end @@ -181,7 +182,9 @@ bool llama_memory_recurrent::seq_rm(llama_seq_id seq_id, llama_pos p0, llama_pos // partial rollback via per-token snapshot index (bounded by n_rs_seq) if (0 < p0 && p0 <= cell.pos && p1 > cell.pos) { const llama_pos rollback = cell.pos - (p0 - 1); - if (rollback >= 1 && rollback <= (llama_pos) n_rs_seq) { + // pending rollback is single-use + const bool pending = rs_idx[seq_id] != 0; + if (!pending && rollback >= 1 && rollback <= (llama_pos) n_rs_seq) { set_rs_idx(seq_id, (uint32_t) rollback); cell.pos = p0 - 1; return true; @@ -390,10 +393,17 @@ llama_pos llama_memory_recurrent::seq_pos_max(llama_seq_id seq_id) const { } void llama_memory_recurrent::set_rs_idx(llama_seq_id seq_id, uint32_t idx) { - if (seq_id < 0 || (size_t) seq_id >= rs_idx.size()) { + if (seq_id < 0) { + std::fill(rs_idx.begin(), rs_idx.end(), 0); return; } - rs_idx[seq_id] = (idx > n_rs_seq) ? n_rs_seq : idx; + + assert(n_seq_max == rs_idx.size()); + + GGML_ASSERT((uint32_t) seq_id < n_seq_max); + GGML_ASSERT(idx <= n_rs_seq); + + rs_idx[seq_id] = idx; } std::map llama_memory_recurrent::memory_breakdown() const { @@ -742,6 +752,7 @@ void llama_memory_recurrent::state_write(llama_io_write_i & io, llama_seq_id seq uint32_t cell_range_begin = size; for (uint32_t i = 0; i < size; ++i) { const auto & cell = cells[i]; + // TODO: fix incosistent handling of `seq_id < 0` and `seq_id == -1` in the codebase [TAG_LLAMA_SEQ_ID_NEG] if ((seq_id == -1 && !cell.is_empty()) || cell.has_seq_id(seq_id)) { ++cell_count; uint32_t rs_idx_cur = 0; @@ -827,6 +838,7 @@ void llama_memory_recurrent::state_read(llama_io_read_i & io, llama_seq_id seq_i } if (!res) { + // TODO: fix incosistent handling of `seq_id < 0` and `seq_id == -1` in the codebase [TAG_LLAMA_SEQ_ID_NEG] if (seq_id == -1) { clear(true); } else { @@ -836,11 +848,7 @@ void llama_memory_recurrent::state_read(llama_io_read_i & io, llama_seq_id seq_i } if (n_rs_seq != 0) { - if (seq_id == -1) { - std::fill(rs_idx.begin(), rs_idx.end(), 0); - } else { - set_rs_idx(seq_id, 0); - } + set_rs_idx(seq_id, 0); } } diff --git a/src/llama-model-saver.cpp b/src/llama-model-saver.cpp index 0d39e6de8..2eb5b7aaf 100644 --- a/src/llama-model-saver.cpp +++ b/src/llama-model-saver.cpp @@ -293,6 +293,14 @@ void llama_model_saver::add_kv_from_model() { add_kv(LLM_KV_ATTENTION_INDEXER_LOCAL_BLOCKS, hparams.indexer_local_blocks); add_kv(LLM_KV_ATTENTION_INDEXER_TYPES, hparams.is_indexer_full_impl, true); add_kv(LLM_KV_ATTENTION_RECURRENT_LAYERS, hparams.is_recr_impl, true); + add_kv(LLM_KV_ATTENTION_OUTPUT_GROUP_COUNT, hparams.dsv4_o_group_count); + add_kv(LLM_KV_ATTENTION_OUTPUT_LORA_RANK, hparams.dsv4_o_lora_rank); + add_kv(LLM_KV_ATTENTION_COMPRESS_ROPE_FREQ_BASE, hparams.dsv4_compress_rope_base); + add_kv(LLM_KV_ATTENTION_COMPRESS_RATIOS, hparams.dsv4_compress_ratios, true); + add_kv(LLM_KV_HYPER_CONNECTION_COUNT, hparams.dsv4_hc_mult); + add_kv(LLM_KV_HYPER_CONNECTION_SINKHORN_ITERATIONS, hparams.dsv4_hc_sinkhorn_iters); + add_kv(LLM_KV_HYPER_CONNECTION_EPSILON, hparams.dsv4_hc_eps); + add_kv(LLM_KV_HASH_LAYER_COUNT, hparams.dsv4_hash_layer_count); const float rope_scaling_factor = hparams.rope_freq_scale_train == 1.0f ? 0.0f : 1.0f/hparams.rope_freq_scale_train; @@ -422,6 +430,9 @@ void llama_model_saver::add_tensors_from_model() { add_tensor(model->cls_out); add_tensor(model->cls_out_b); add_tensor(model->cls_norm); + add_tensor(model->hc_head_fn); + add_tensor(model->hc_head_base); + add_tensor(model->hc_head_scale); for (const struct llama_layer & layer : model->layers) { for (size_t i = 0; i < sizeof(layer)/sizeof(struct ggml_tensor *); ++i) { diff --git a/src/models/deepseek4.cpp b/src/models/deepseek4.cpp index 1c278e435..fc816e2ae 100644 --- a/src/models/deepseek4.cpp +++ b/src/models/deepseek4.cpp @@ -1,3 +1,4 @@ +#include "llama-hparams.h" #include "models.h" #include "llama-kv-cache-dsv4.h" @@ -58,6 +59,7 @@ void llama_model_deepseek4::load_arch_hparams(llama_model_loader & ml) { if (n_compress_ratios < hparams.n_layer_all) { throw std::runtime_error("DeepSeek-V4 compress_ratios is shorter than block_count"); } + GGML_ASSERT(n_compress_ratios <= LLAMA_MAX_LAYERS); ml.get_arr(LLM_KV_ATTENTION_COMPRESS_RATIOS, hparams.dsv4_compress_ratios); ml.get_key(LLM_KV_EXPERT_GATING_FUNC, hparams.expert_gating_func); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e517d2c63..cb6ae2970 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -228,6 +228,15 @@ if (NOT WIN32 OR NOT BUILD_SHARED_LIBS) set_tests_properties(test-recurrent-state-rollback-nemotron-h PROPERTIES FIXTURES_REQUIRED generate-models ) + llama_test( + test-recurrent-state-rollback + NAME test-recurrent-state-rollback-dsv4 + LABEL main + ARGS -m "${MODEL_DIR}/deepseek4-moe.gguf" + ) + set_tests_properties(test-recurrent-state-rollback-dsv4 PROPERTIES + FIXTURES_REQUIRED generate-models + ) endif() llama_build_and_test(test-chat-peg-parser.cpp peg-parser/simple-tokenize.cpp) diff --git a/tests/test-llama-archs.cpp b/tests/test-llama-archs.cpp index 18676f2be..dff8c4668 100644 --- a/tests/test-llama-archs.cpp +++ b/tests/test-llama-archs.cpp @@ -101,6 +101,11 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) { n_head = 1; n_ff = 96; n_layer = 22; // hparams.n_layer_kv_from_start = 20 is hardcoded + } else if (arch == LLM_ARCH_DEEPSEEK4) { + n_embd = 128; + n_head = 1; + n_ff = 192; + n_layer = 3; // uncompressed + csa + hca, one layer of each ratio kind } else if (arch == LLM_ARCH_STEP35 || arch == LLM_ARCH_LAGUNA) { n_embd = 160; // exercise per-head tensor split granularity with head size 80 } else if (arch == LLM_ARCH_QWEN3 || arch == LLM_ARCH_MUSE_GLIMMER || arch == LLM_ARCH_AFMOE) { @@ -203,6 +208,10 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) { } ms.add_kv(LLM_KV_ATTENTION_INDEXER_TYPES, indexer_types); } + } else if (arch == LLM_ARCH_DEEPSEEK4) { + ms.add_kv(LLM_KV_ATTENTION_KEY_LENGTH, uint32_t(128)); + ms.add_kv(LLM_KV_ATTENTION_VALUE_LENGTH, uint32_t(128)); + ms.add_kv(LLM_KV_ROPE_DIMENSION_COUNT, uint32_t(64)); } else if (arch == LLM_ARCH_MINIMAX_M3) { // partial rotary: n_rot must not exceed the indexer key length (64) ms.add_kv(LLM_KV_ROPE_DIMENSION_COUNT, uint32_t(64)); @@ -239,6 +248,20 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) { // MSA requires one indexer head per GQA (KV) head, unlike the DSA archs where the // indexer head count is independent of the main attention head count. + if (arch == LLM_ARCH_DEEPSEEK4) { + ms.add_kv(LLM_KV_EXPERT_WEIGHTS_SCALE, 2.5f); + ms.add_kv(LLM_KV_EXPERT_WEIGHTS_NORM, true); + ms.add_kv(LLM_KV_SWIGLU_CLAMP_EXP, 7.0f); + ms.add_kv(LLM_KV_ATTENTION_OUTPUT_GROUP_COUNT, uint32_t(1)); + ms.add_kv(LLM_KV_ATTENTION_OUTPUT_LORA_RANK, uint32_t(64)); + ms.add_kv(LLM_KV_ATTENTION_COMPRESS_ROPE_FREQ_BASE, 10000.0f); + ms.add_kv(LLM_KV_HYPER_CONNECTION_COUNT, uint32_t(4)); + ms.add_kv(LLM_KV_HYPER_CONNECTION_SINKHORN_ITERATIONS, uint32_t(4)); + ms.add_kv(LLM_KV_HYPER_CONNECTION_EPSILON, 1e-6f); + ms.add_kv(LLM_KV_HASH_LAYER_COUNT, uint32_t(0)); + ms.add_kv(LLM_KV_ATTENTION_COMPRESS_RATIOS, std::vector({0, 4, 128})); + } + ms.add_kv(LLM_KV_ATTENTION_INDEXER_HEAD_COUNT, arch == LLM_ARCH_MINIMAX_M3 ? n_head : uint32_t(1)); ms.add_kv(LLM_KV_ATTENTION_INDEXER_KEY_LENGTH, uint32_t(64)); ms.add_kv(LLM_KV_ATTENTION_INDEXER_TOP_K, uint32_t(8)); @@ -257,7 +280,7 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) { ms.add_kv(LLM_KV_EXPERT_COUNT, uint32_t(2)); ms.add_kv(LLM_KV_EXPERT_USED_COUNT, uint32_t(1)); ms.add_kv(LLM_KV_EXPERT_SHARED_COUNT, uint32_t(1)); - ms.add_kv(LLM_KV_EXPERT_GATING_FUNC, uint32_t(2)); // sigmoid + ms.add_kv(LLM_KV_EXPERT_GATING_FUNC, arch == LLM_ARCH_DEEPSEEK4 ? uint32_t(4) : uint32_t(2)); // sqrtsoftplus : sigmoid ms.add_kv(LLM_KV_EXPERT_GROUP_SCALE, 1.0f); ms.add_kv(LLM_KV_EXPERTS_PER_GROUP, uint32_t(1)); } @@ -395,6 +418,7 @@ static bool moe_mandatory(const llm_arch arch) { case LLM_ARCH_DEEPSEEK2: case LLM_ARCH_DEEPSEEK32: case LLM_ARCH_DOTS3NOTE: + case LLM_ARCH_DEEPSEEK4: case LLM_ARCH_GLM4_MOE: case LLM_ARCH_GLM_DSA: case LLM_ARCH_EXAONE_MOE: @@ -480,9 +504,6 @@ static bool arch_supported(const llm_arch arch) { if (arch == LLM_ARCH_DEEPSEEK2OCR) { return false; } - if (arch == LLM_ARCH_DEEPSEEK4) { - return false; - } // FIXME: these hit scheduler/view-backed-output issues with WebGPU on CI. #ifdef GGML_USE_WEBGPU diff --git a/tests/test-recurrent-state-rollback.cpp b/tests/test-recurrent-state-rollback.cpp index 5d1f0140b..c6f599e58 100644 --- a/tests/test-recurrent-state-rollback.cpp +++ b/tests/test-recurrent-state-rollback.cpp @@ -35,6 +35,178 @@ static bool decode_one(llama_context * ctx, llama_token tok, llama_pos pos) { return ok; } +// Roll back multiple sequences, then replay them in a single batch whose +// per-seq token count exceeds n_ubatch: each seq's replay spans several +// ubatches while its rollback restore is still pending. Compared against a +// reference context that never advanced past the rollback point and decodes +// the identical replay batch. +static bool test_multi_seq_split_replay(const common_params & params, llama_model * model, const int n_vocab) { + constexpr uint32_t n_seqs = 2; + constexpr uint32_t n_ubatch = 16; + constexpr uint32_t n_prompt = 19; + constexpr uint32_t n_rollback = 3; + constexpr uint32_t n_replay = 40; // > n_ubatch so each seq spans multiple ubatches + constexpr llama_pos p0 = n_prompt - n_rollback; + + const auto make_ctx_multi = [&]() { + auto cparams = common_context_params_to_llama(params); + cparams.n_seq_max = n_seqs; + cparams.n_rs_seq = 8; + cparams.n_ctx = 256; + cparams.n_batch = 256; + cparams.n_ubatch = n_ubatch; + cparams.kv_unified = false; + return llama_init_from_model(model, cparams); + }; + + llama_context * ctx_roll = make_ctx_multi(); + llama_context * ctx_ref = make_ctx_multi(); + if (ctx_roll == nullptr || ctx_ref == nullptr) { + fprintf(stderr, "%s : failed to init multi-seq contexts\n", __func__); + return false; + } + + const auto cleanup = [&]() { + llama_free(ctx_roll); + llama_free(ctx_ref); + }; + + if (llama_n_rs_seq(ctx_roll) < n_rollback) { + fprintf(stderr, "%s : skipping because n_rs_seq is too small\n", __func__); + cleanup(); + return true; + } + + const auto tok = [&](uint32_t seq, llama_pos pos) { + return (llama_token) ((7*(uint32_t) pos + 31*seq + 1) % (uint32_t) n_vocab); + }; + + bool ok = true; + + // both contexts decode the identical [0, p0) prefill; only ctx_roll decodes + // the tail, which is then rolled back so its restore is pending at replay + for (uint32_t s = 0; s < n_seqs && ok; ++s) { + llama_batch batch = llama_batch_init(n_prompt, 0, 1); + for (llama_pos pos = 0; pos < (llama_pos) p0; ++pos) { + common_batch_add(batch, tok(s, pos), pos, { (llama_seq_id) s }, false); + } + ok = ok && llama_decode(ctx_roll, batch) == 0; + ok = ok && llama_decode(ctx_ref, batch) == 0; + + common_batch_clear(batch); + for (llama_pos pos = p0; pos < (llama_pos) n_prompt; ++pos) { + common_batch_add(batch, tok(s, pos), pos, { (llama_seq_id) s }, false); + } + ok = ok && llama_decode(ctx_roll, batch) == 0; + llama_batch_free(batch); + + ok = ok && llama_memory_seq_rm(llama_get_memory(ctx_roll), (llama_seq_id) s, p0, -1); + + // a second partial removal while one is pending must be refused + ok = ok && !llama_memory_seq_rm(llama_get_memory(ctx_roll), (llama_seq_id) s, p0 - 1, -1); + } + if (!ok) { + fprintf(stderr, "%s : multi-seq prefill/rollback failed\n", __func__); + cleanup(); + return false; + } + + llama_batch batch = llama_batch_init(n_seqs*n_replay, 0, 1); + for (uint32_t s = 0; s < n_seqs; ++s) { + for (uint32_t i = 0; i < n_replay; ++i) { + const llama_pos pos = p0 + (llama_pos) i; + common_batch_add(batch, tok(s, pos), pos, { (llama_seq_id) s }, true); + } + } + ok = llama_decode(ctx_roll, batch) == 0; + ok = ok && llama_decode(ctx_ref, batch) == 0; + llama_batch_free(batch); + if (!ok) { + fprintf(stderr, "%s : multi-seq replay decode failed\n", __func__); + cleanup(); + return false; + } + + // identical ubatch shapes from bit-exact states: a correct implementation + // matches bitwise, so eps only allows backend scheduling noise + constexpr float eps = 1e-7f; + + float diff_max = 0.0f; + uint32_t seq_first = 0; + int32_t pos_first = -1; + for (uint32_t i = 0; i < n_seqs*n_replay; ++i) { + const float * l_roll = llama_get_logits_ith(ctx_roll, i); + const float * l_ref = llama_get_logits_ith(ctx_ref, i); + if (l_roll == nullptr || l_ref == nullptr) { + fprintf(stderr, "%s : missing multi-seq logits at index %u\n", __func__, i); + cleanup(); + return false; + } + for (int t = 0; t < n_vocab; ++t) { + const float diff = std::fabs(l_roll[t] - l_ref[t]); + if (diff > eps && pos_first < 0) { + seq_first = i/n_replay; + pos_first = p0 + (int32_t) (i%n_replay); + } + diff_max = std::max(diff_max, diff); + } + } + + if (diff_max > eps) { + fprintf(stderr, "%s : multi-seq split replay logits mismatch (max diff %g, first at seq %u pos %d)\n", + __func__, (double) diff_max, seq_first, pos_first); + cleanup(); + return false; + } + + fprintf(stderr, "%s : multi-seq split replay matched (max diff %g)\n", __func__, (double) diff_max); + + // seq-1-only decodes must be independent of seq 0's content: diverge seq 0 + // in ctx_ref only, then compare identical seq-1-only continuations bitwise + constexpr uint32_t n_tail = 4; + + { + llama_batch batch_tail = llama_batch_init(n_tail, 0, 1); + for (uint32_t i = 0; i < n_tail; ++i) { + const llama_pos pos = p0 + (llama_pos) (n_replay + i); + common_batch_add(batch_tail, tok(0, pos + 7), pos, { 0 }, false); + } + ok = llama_decode(ctx_ref, batch_tail) == 0; + llama_batch_free(batch_tail); + } + + float diff_tail = 0.0f; + for (uint32_t i = 0; i < n_tail && ok; ++i) { + const llama_pos pos = p0 + (llama_pos) (n_replay + i); + llama_batch batch_one = llama_batch_init(1, 0, 1); + common_batch_add(batch_one, tok(1, pos), pos, { 1 }, true); + ok = llama_decode(ctx_roll, batch_one) == 0; + ok = ok && llama_decode(ctx_ref, batch_one) == 0; + llama_batch_free(batch_one); + if (!ok) { + break; + } + + const float * l_roll = llama_get_logits_ith(ctx_roll, 0); + const float * l_ref = llama_get_logits_ith(ctx_ref, 0); + ok = l_roll != nullptr && l_ref != nullptr; + for (int t = 0; ok && t < n_vocab; ++t) { + diff_tail = std::max(diff_tail, std::fabs(l_roll[t] - l_ref[t])); + } + } + + if (!ok || diff_tail > eps) { + fprintf(stderr, "%s : seq-1-only decode leaked seq 0 state (ok=%d, max diff %g)\n", + __func__, ok ? 1 : 0, (double) diff_tail); + cleanup(); + return false; + } + + fprintf(stderr, "%s : seq-1-only decode independent of seq 0 (max diff %g)\n", __func__, (double) diff_tail); + cleanup(); + return true; +} + int main(int argc, char ** argv) { std::setlocale(LC_NUMERIC, "C"); @@ -220,5 +392,10 @@ int main(int argc, char ** argv) { llama_free(ctx_src); llama_free(ctx_dst); llama_free(ctx_dirty); + + if (!test_multi_seq_split_replay(params, model, n_vocab)) { + return 1; + } + return 0; } From ba8e0eddfb8e4791915e5e1f77a66615e30e7d2e Mon Sep 17 00:00:00 2001 From: Bartosz Taudul Date: Sun, 23 Aug 2026 14:39:16 +0200 Subject: [PATCH 10/28] common : skip device_info loop if it's not going to be printed (#26692) The device_info loop iterates over the discovered devices and gets the available and total memory counts. With the CUDA backend (and possibly others too) this requires creating a GPU context, which, in case of CUDA, results in a 550 MB VRAM allocation. For this information to be used in any way, the log verbosity must be set to LOG_LEVEL_TRACE. If it's not, including in the default configuration, the contexts get created, memory sizes get queried, then the log function quietly discards the data. In certain cases the user may not want to use any GPU resources. The device_loop iteration is the only place touching the GPU that cannot be skipped. Fix by checking the verbosity level and skipping the loop if there would be no output. --- common/common.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/common/common.cpp b/common/common.cpp index d84d57ac9..3d54bd600 100644 --- a/common/common.cpp +++ b/common/common.cpp @@ -402,10 +402,11 @@ void common_params_print_info(const common_params & params, bool print_devices) #endif COM_TRC("%s: build %d (%s) with %s for %s%s\n", __func__, llama_build_number(), llama_commit(), llama_compiler(), llama_build_target(), build_type); - COM_INF("%s: verbosity = %d (adjust with the `-lv N` CLI arg)\n", __func__, common_log_get_verbosity_thold()); + const int verbosity = common_log_get_verbosity_thold(); + COM_INF("%s: verbosity = %d (adjust with the `-lv N` CLI arg)\n", __func__, verbosity); // device enumeration creates a primary context on CUDA backends, skip it when the caller does not own any device - if (print_devices) { + if (print_devices && verbosity >= LOG_LEVEL_TRACE) { COM_TRC("%s", "device_info:\n"); for (size_t i = 0; i < ggml_backend_dev_count(); ++i) { auto * dev = ggml_backend_dev_get(i); From e8eed4525aaca00a78d8f837dce90dd4d4708133 Mon Sep 17 00:00:00 2001 From: Georgi Gerganov Date: Sun, 23 Aug 2026 15:55:51 +0300 Subject: [PATCH 11/28] server : add LLAMA_SERVER_SLOTS_N_DIFF (#27600) --- tools/server/server-context.cpp | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tools/server/server-context.cpp b/tools/server/server-context.cpp index 572682af7..a9edbd7be 100644 --- a/tools/server/server-context.cpp +++ b/tools/server/server-context.cpp @@ -858,8 +858,10 @@ private: // slots / clients std::vector slots; - int trace = 0; - int slots_debug = 0; + int trace = 0; // env: LLAMA_TRACE + int slots_debug = 0; // env: LLAMA_SERVER_SLOTS_DEBUG + int slots_n_diff = 0; // env: LLAMA_SERVER_SLOTS_N_DIFF + int n_empty_consecutive = 0; std::unique_ptr prompt_cache; @@ -1247,6 +1249,15 @@ private: } } + { + const char * LLAMA_SERVER_SLOTS_N_DIFF = getenv("LLAMA_SERVER_SLOTS_N_DIFF"); + slots_n_diff = LLAMA_SERVER_SLOTS_N_DIFF ? atoi(LLAMA_SERVER_SLOTS_N_DIFF) : 0; + + if (slots_n_diff) { + SRV_WRN("LLAMA_SERVER_SLOTS_N_DIFF = %d\n", slots_n_diff); + } + } + // the update_slots() logic will always submit a maximum of n_batch or n_parallel tokens // note that n_batch can be > n_ctx (e.g. for non-causal attention models such as BERT where the KV cache is not used) { @@ -3179,8 +3190,8 @@ private: // when the prompt prefix does not match, print the tokens around the mismatch // this is useful for debugging prompt caching if (slots_debug) { - const int np0 = std::max(n_past - 4, 0); - const int np1 = std::min(n_past + 6, std::min(slot.prompt.tokens.size(), slot.task->tokens.size())); + const int np0 = std::max(n_past - slots_n_diff, 0); + const int np1 = std::min(n_past + slots_n_diff + 2, std::min(slot.prompt.tokens.size(), slot.task->tokens.size())); std::stringstream ss0; std::stringstream ss1; From a278dcef04f2ce950d3a17d8aa76d4ef86483e0e Mon Sep 17 00:00:00 2001 From: Niklas Wenzel Date: Sun, 23 Aug 2026 14:56:47 +0200 Subject: [PATCH 12/28] contrib : recommend waiting for CI before merging (#27603) --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 003133478..6aac3cb87 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,6 +74,7 @@ For more info, please refer to the [AGENTS.md](AGENTS.md) file. - If a PR does not warrant a new release, add `[no release]` in the squashed commit to spare CI resources - Be mindful of maintenance: most of the work going into a feature happens after the PR is merged. If the PR author is not committed to contribute long-term, someone else needs to take responsibility (you) - Add the ["merge ready"](https://github.com/ggml-org/llama.cpp/pulls?q=is%3Apr+is%3Aopen+draft%3Ano+sort%3Aupdated-desc+label%3A%22merge+ready%22+) label to a PR to indicate when a PR can be fast-merged without waiting for 2 independent reviews. [(more info)](https://github.com/ggml-org/llama.cpp/pull/26178) +- Wait for CI results before merging Maintainers reserve the right to decline review or close pull requests for any reason, without any questions, particularly under any of the following conditions: - The proposed change is already mentioned in the roadmap or an existing issue, and it has been assigned to someone. From 95b8e33e16bb9a60de780a70930ebf729db6a90a Mon Sep 17 00:00:00 2001 From: Georgi Gerganov Date: Sun, 23 Aug 2026 15:57:07 +0300 Subject: [PATCH 13/28] ci : add test-llama-archs tensor split for Metal (#27598) Run test-llama-archs with 1 to 4 GGML_METAL_DEVICES, mirroring the existing CUDA runs, and dispatch the job unconditionally since the per-backend guards now decide what to run. Assisted-by: llama.cpp:DeepSeek-v4-Flash-0731 --- ci/run.sh | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/ci/run.sh b/ci/run.sh index 02ce54916..1f1e4bc03 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -307,10 +307,19 @@ function gg_run_test_llama_archs_tensor_split { set -e - GGML_CUDA_DEVICES=1 ./build-ci-release/bin/test-llama-archs -s 1 2>&1 - GGML_CUDA_DEVICES=2 ./build-ci-release/bin/test-llama-archs -s 1 2>&1 - GGML_CUDA_DEVICES=3 ./build-ci-release/bin/test-llama-archs -s 1 2>&1 - GGML_CUDA_DEVICES=4 ./build-ci-release/bin/test-llama-archs -s 1 2>&1 + if [ ! -z ${GG_BUILD_CUDA} ]; then + GGML_CUDA_DEVICES=1 ./build-ci-release/bin/test-llama-archs -s 1 2>&1 + GGML_CUDA_DEVICES=2 ./build-ci-release/bin/test-llama-archs -s 1 2>&1 + GGML_CUDA_DEVICES=3 ./build-ci-release/bin/test-llama-archs -s 1 2>&1 + GGML_CUDA_DEVICES=4 ./build-ci-release/bin/test-llama-archs -s 1 2>&1 + fi + + if [ ! -z ${GG_BUILD_METAL} ]; then + GGML_METAL_DEVICES=1 ./build-ci-release/bin/test-llama-archs -s 1 2>&1 + GGML_METAL_DEVICES=2 ./build-ci-release/bin/test-llama-archs -s 1 2>&1 + GGML_METAL_DEVICES=3 ./build-ci-release/bin/test-llama-archs -s 1 2>&1 + GGML_METAL_DEVICES=4 ./build-ci-release/bin/test-llama-archs -s 1 2>&1 + fi set +e } @@ -318,7 +327,7 @@ function gg_run_test_llama_archs_tensor_split { function gg_sum_test_llama_archs_tensor_split { gg_printf '### %s\n\n' "${ci}" - gg_printf 'Runs test-llama-archs with 1 to 4 CUDA devices\n' + gg_printf 'Runs test-llama-archs with 1 to 4 devices\n' gg_printf '- status: %s\n' "$(cat $OUT/${ci}.exit)" gg_printf '```\n' gg_printf '%s\n' "$(cat $OUT/${ci}.log)" @@ -776,9 +785,7 @@ ret=0 test $ret -eq 0 && gg_run ctest_debug test $ret -eq 0 && gg_run ctest_release -if [ ! -z ${GG_BUILD_CUDA} ]; then - test $ret -eq 0 && gg_run test_llama_archs_tensor_split -fi +test $ret -eq 0 && gg_run test_llama_archs_tensor_split if [ ! -z ${GG_BUILD_HIGH_PERF} ]; then test $ret -eq 0 && gg_run test_backend_ops_cpu From 56db501e73cfb10c8fcce61be708f5c3ee749271 Mon Sep 17 00:00:00 2001 From: Xuan-Son Nguyen Date: Sun, 23 Aug 2026 18:35:41 +0200 Subject: [PATCH 14/28] mtmd: use pillow-accurate algo, correct resize_algo for all models (#27594) * mtmd: use pillow-accurate resize algo, correct resize_algo for all models * speed optimization --- tools/mtmd/clip-model.h | 8 +- tools/mtmd/clip.cpp | 39 ++--- tools/mtmd/mtmd-image.cpp | 326 ++++++++++---------------------------- 3 files changed, 106 insertions(+), 267 deletions(-) diff --git a/tools/mtmd/clip-model.h b/tools/mtmd/clip-model.h index fcdabd633..060938d86 100644 --- a/tools/mtmd/clip-model.h +++ b/tools/mtmd/clip-model.h @@ -29,10 +29,10 @@ enum patch_merge_type { PATCH_MERGE_SPATIAL_UNPAD, }; +// all algos are Pillow-compatible (matching PIL.Image.resize output) enum resize_algo { - RESIZE_ALGO_BILINEAR, // stretch to target resolution - RESIZE_ALGO_BICUBIC, // center-crop when aspect ratio doesn't match - RESIZE_ALGO_BICUBIC_PILLOW, + RESIZE_ALGO_BILINEAR, + RESIZE_ALGO_BICUBIC, RESIZE_ALGO_LANCZOS, }; @@ -73,7 +73,7 @@ struct clip_hparams { int32_t preproc_max_tiles = 0; int32_t preproc_tile_size = 0; // local tile size (deepseek-ocr) resize_algo image_resize_algo_rf = RESIZE_ALGO_BICUBIC; - resize_algo image_resize_algo_ov = RESIZE_ALGO_BILINEAR; + resize_algo image_resize_algo_ov = RESIZE_ALGO_BICUBIC; pad_style image_pad_rf = PAD_CEIL; // padding style for the refined image (e.g. llava-1.6) pad_style image_pad_ov = PAD_NONE; // padding style for the overview image (e.g. llava-1.6) std::array image_pad_color_rf = {0, 0, 0}; // padding color for refined image diff --git a/tools/mtmd/clip.cpp b/tools/mtmd/clip.cpp index 89ca65a7b..90de19575 100644 --- a/tools/mtmd/clip.cpp +++ b/tools/mtmd/clip.cpp @@ -1420,20 +1420,18 @@ struct clip_model_loader { hparams.image_pad_color = {122, 116, 104}; if (!hparams.image_res_candidates.empty()) { hparams.image_resize_pad = PAD_CEIL; - hparams.image_resize_algo = RESIZE_ALGO_BILINEAR; + hparams.image_resize_algo = RESIZE_ALGO_BICUBIC; } else { // llava-1.6 default params hparams.image_pad_ov = PAD_NONE; hparams.image_pad_rf = PAD_CEIL; hparams.image_pad_color_rf = {122, 116, 104}; - hparams.image_resize_algo_rf = RESIZE_ALGO_BICUBIC; - hparams.image_resize_algo_ov = RESIZE_ALGO_BILINEAR; } } break; case PROJECTOR_TYPE_GLM_EDGE: { hparams.image_resize_pad = PAD_CEIL; - hparams.image_resize_algo = RESIZE_ALGO_BILINEAR; + hparams.image_resize_algo = RESIZE_ALGO_BICUBIC; } break; case PROJECTOR_TYPE_MINICPMV: { @@ -1490,6 +1488,7 @@ struct clip_model_loader { case PROJECTOR_TYPE_IDEFICS3: { // use default llava-uhd preprocessing params + hparams.image_resize_algo = RESIZE_ALGO_LANCZOS; get_u32(KEY_PROJ_SCALE_FACTOR, hparams.n_merge, false); get_u32(KEY_PREPROC_IMAGE_SIZE, hparams.image_longest_edge, false); hparams.set_limit_image_tokens(); @@ -1516,7 +1515,7 @@ struct clip_model_loader { // ref: https://huggingface.co/mistral-community/pixtral-12b/blob/main/preprocessor_config.json // TODO: verify the image_min_tokens hparams.n_merge = 1; // the original pixtral does not use patch merging - hparams.image_resize_algo = RESIZE_ALGO_BILINEAR; + hparams.image_resize_algo = RESIZE_ALGO_BICUBIC; hparams.rope_theta = 10000.0f; get_u32(KEY_SPATIAL_MERGE_SIZE, hparams.n_merge, false); hparams.set_limit_image_tokens(8, 1024); @@ -1544,7 +1543,7 @@ struct clip_model_loader { case PROJECTOR_TYPE_DOTS3NOTE_V: { hparams.rope_theta = 10000.0f; - hparams.image_resize_algo = RESIZE_ALGO_BICUBIC_PILLOW; + hparams.image_resize_algo = RESIZE_ALGO_BICUBIC; get_u32(KEY_SPATIAL_MERGE_SIZE, hparams.n_merge); get_u32(KEY_IMAGE_MIN_PIXELS, hparams.image_min_pixels); get_u32(KEY_IMAGE_MAX_PIXELS, hparams.image_max_pixels); @@ -1562,7 +1561,7 @@ struct clip_model_loader { } break; case PROJECTOR_TYPE_KIMIVL: { - hparams.image_resize_algo = RESIZE_ALGO_BILINEAR; + hparams.image_resize_algo = RESIZE_ALGO_BICUBIC; hparams.rope_theta = 10000.0f; get_u32(KEY_PROJ_SCALE_FACTOR, hparams.n_merge, false); // TODO: check kimivl preprocessor for exact values @@ -1601,7 +1600,7 @@ struct clip_model_loader { { hparams.rope_theta = 100.0f; hparams.n_merge = 3; // pooling_kernel_size - hparams.image_resize_algo = RESIZE_ALGO_BILINEAR; + hparams.image_resize_algo = RESIZE_ALGO_BICUBIC; get_u32(KEY_PROJ_SCALE_FACTOR, hparams.n_merge, false); if (model.proj_type == PROJECTOR_TYPE_GEMMA4UV) { // for "unified" variant, we directly use a bigger patch size, because the "token merging" is done directly on conv layer @@ -1618,6 +1617,7 @@ struct clip_model_loader { // Gemma3n uses MobileNetV5 which produces 256 tokens (16x16) // Similar configuration to Gemma3 hparams.n_merge = 1; // MobileNetV5 handles resizing internally + hparams.image_resize_algo = RESIZE_ALGO_BILINEAR; get_u32(KEY_PROJ_SCALE_FACTOR, hparams.n_merge, false); } break; case PROJECTOR_TYPE_QWEN2VL: @@ -1625,7 +1625,7 @@ struct clip_model_loader { case PROJECTOR_TYPE_QWEN3VL: { hparams.n_merge = 2; // default value for Qwen 2 and 2.5 - hparams.image_resize_algo = RESIZE_ALGO_BILINEAR; + hparams.image_resize_algo = RESIZE_ALGO_BICUBIC; get_u32(KEY_SPATIAL_MERGE_SIZE, hparams.n_merge, false); get_u32(KEY_WIN_ATTN_PATTERN, hparams.n_wa_pattern, model.proj_type == PROJECTOR_TYPE_QWEN25VL); // only 2.5 requires it // ref: https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct/blob/main/preprocessor_config.json @@ -1641,7 +1641,7 @@ struct clip_model_loader { case PROJECTOR_TYPE_MINIMAX_M3: { hparams.n_merge = 2; // spatial_merge_size - hparams.image_resize_algo = RESIZE_ALGO_BICUBIC_PILLOW; + hparams.image_resize_algo = RESIZE_ALGO_BICUBIC; hparams.image_resize_pad = PAD_NONE; get_u32(KEY_SPATIAL_MERGE_SIZE, hparams.n_merge, false); // n_merge is used as a divisor in clip_image_batch_encode @@ -1666,7 +1666,7 @@ struct clip_model_loader { case PROJECTOR_TYPE_MIMOVL: { hparams.n_merge = 2; // spatial_merge_size - hparams.image_resize_algo = RESIZE_ALGO_BICUBIC_PILLOW; + hparams.image_resize_algo = RESIZE_ALGO_BICUBIC; get_u32(KEY_SPATIAL_MERGE_SIZE, hparams.n_merge, false); get_u32(string_format(KEY_N_HEAD_KV, "vision"), hparams.n_head_kv); // 1D banded sliding-window radius (visual_token_window_size); required @@ -1713,15 +1713,15 @@ struct clip_model_loader { log_ffn_op = "gelu_erf"; hparams.image_resize_algo = RESIZE_ALGO_BICUBIC; - // reka model performs better when using resize_bicubic, which stretches - // the image to fit fixed square size + // reka model performs better when the image is stretched to fit + // fixed square size (no padding) hparams.image_resize_pad = PAD_NONE; } break; case PROJECTOR_TYPE_GLM4V: { hparams.rope_theta = 10000.0f; hparams.n_merge = 2; // default value for GLM4-V - hparams.image_resize_algo = RESIZE_ALGO_BILINEAR; + hparams.image_resize_algo = RESIZE_ALGO_BICUBIC; get_u32(KEY_SPATIAL_MERGE_SIZE, hparams.n_merge, false); hparams.set_limit_image_tokens(8, 4096); hparams.set_warmup_n_tokens(46*46); // avoid OOM on warmup @@ -1729,6 +1729,7 @@ struct clip_model_loader { case PROJECTOR_TYPE_LLAMA4: { hparams.rope_theta = 10000.0f; + hparams.image_resize_algo = RESIZE_ALGO_BILINEAR; get_u32(KEY_PROJ_SCALE_FACTOR, hparams.n_merge, false); set_llava_uhd_res_candidates(model, 3); } break; @@ -1840,7 +1841,7 @@ struct clip_model_loader { case PROJECTOR_TYPE_PADDLEOCR: { hparams.n_merge = 2; - hparams.image_resize_algo = RESIZE_ALGO_BILINEAR; + hparams.image_resize_algo = RESIZE_ALGO_BICUBIC; get_u32(KEY_IMAGE_MIN_PIXELS, hparams.image_min_pixels); get_u32(KEY_IMAGE_MAX_PIXELS, hparams.image_max_pixels); @@ -1852,7 +1853,7 @@ struct clip_model_loader { hparams.patch_size = 16; hparams.image_size = 1024; hparams.warmup_image_size = 1024; - hparams.image_resize_algo = RESIZE_ALGO_BICUBIC_PILLOW; + hparams.image_resize_algo = RESIZE_ALGO_BICUBIC; hparams.image_pad_color = {127, 127, 127}; get_u32(KEY_SAM_N_BLOCK, hparams.sam_n_layer, true); @@ -1882,7 +1883,7 @@ struct clip_model_loader { case PROJECTOR_TYPE_HUNYUANVL: { hparams.n_merge = 2; - hparams.image_resize_algo = RESIZE_ALGO_BICUBIC_PILLOW; + hparams.image_resize_algo = RESIZE_ALGO_LANCZOS; hparams.image_resize_pad = PAD_NONE; hparams.ffn_op = FFN_GELU; hparams.set_limit_image_tokens(256, 16384); @@ -1955,12 +1956,12 @@ struct clip_model_loader { case PROJECTOR_TYPE_JANUS_PRO: { hparams.image_pad_color = {127, 127, 127}; - hparams.image_resize_algo = RESIZE_ALGO_BILINEAR; + hparams.image_resize_algo = RESIZE_ALGO_BICUBIC; } break; case PROJECTOR_TYPE_GRANITE4_VISION: { // SigLIP tower. - hparams.image_resize_algo = RESIZE_ALGO_BICUBIC_PILLOW; + hparams.image_resize_algo = RESIZE_ALGO_BICUBIC; hparams.image_resize_pad = PAD_CEIL; // NOTE: feature_layers loaded in common path as optional diff --git a/tools/mtmd/mtmd-image.cpp b/tools/mtmd/mtmd-image.cpp index 0d9db4f62..0dda8770f 100644 --- a/tools/mtmd/mtmd-image.cpp +++ b/tools/mtmd/mtmd-image.cpp @@ -58,22 +58,7 @@ struct img_tool { if (padding == PAD_NONE) { // direct resize - switch (algo) { - case RESIZE_ALGO_BILINEAR: - resize_bilinear(src, dst, target_resolution.width, target_resolution.height); - break; - case RESIZE_ALGO_BICUBIC: - resize_bicubic(src, dst, target_resolution.width, target_resolution.height); - break; - case RESIZE_ALGO_BICUBIC_PILLOW: - resize_bicubic_pillow(src, dst, target_resolution.width, target_resolution.height); - break; - case RESIZE_ALGO_LANCZOS: - resize_lanczos_pillow(src, dst, target_resolution.width, target_resolution.height); - break; - default: - throw std::runtime_error("Unsupported resize algorithm"); - } + resize_pillow(src, dst, target_resolution.width, target_resolution.height, algo); } else { // resize with padding clip_image_u8 resized_image; @@ -90,22 +75,7 @@ struct img_tool { new_height = std::min(static_cast(std::ceil(src.get_size().height * scale)), target_resolution.height); } - switch (algo) { - case RESIZE_ALGO_BILINEAR: - resize_bilinear(src, resized_image, new_width, new_height); - break; - case RESIZE_ALGO_BICUBIC: - resize_bicubic(src, resized_image, new_width, new_height); - break; - case RESIZE_ALGO_BICUBIC_PILLOW: - resize_bicubic_pillow(src, resized_image, new_width, new_height); - break; - case RESIZE_ALGO_LANCZOS: - resize_lanczos_pillow(src, resized_image, new_width, new_height); - break; - default: - throw std::runtime_error("Unsupported resize algorithm"); - } + resize_pillow(src, resized_image, new_width, new_height, algo); // fill dst with pad_color fill(dst, pad_color); @@ -224,152 +194,37 @@ struct img_tool { } private: - // Bilinear resize function - static void resize_bilinear(const clip_image_u8 & src, clip_image_u8 & dst, int target_width, int target_height) { - const auto src_size = src.get_size(); - if (src_size.width == 0 || src_size.height == 0) { dst.set_size({0, 0}, false); return; } - if (target_width <= 0) target_width = 1; - if (target_height <= 0) target_height = 1; - - dst.set_size({target_width, target_height}, false); - - if (src.is_placeholder()) { - // no-op for placeholder image, just set the size and return - return; - } - - float x_ratio = target_width > 1 ? static_cast(src_size.width - 1) / (target_width - 1) : 0.0f; - float y_ratio = target_height > 1 ? static_cast(src_size.height - 1) / (target_height - 1) : 0.0f; - - for (int y = 0; y < target_height; ++y) { - for (int x = 0; x < target_width; ++x) { - float px = x * x_ratio; - float py = y * y_ratio; - - int x0 = std::min(static_cast(px), src_size.width - 1); - int y0 = std::min(static_cast(py), src_size.height - 1); - int x1 = std::min(x0 + 1, src_size.width - 1); - int y1 = std::min(y0 + 1, src_size.height - 1); - - float xf = px - x0; - float yf = py - y0; - - const auto p00 = src.get_pixel(x0, y0); - const auto p10 = src.get_pixel(x1, y0); - const auto p01 = src.get_pixel(x0, y1); - const auto p11 = src.get_pixel(x1, y1); - - std::array pixel; - for (int c = 0; c < 3; ++c) { - float top = lerp(static_cast(p00[c]), static_cast(p10[c]), xf); - float bottom = lerp(static_cast(p01[c]), static_cast(p11[c]), xf); - pixel[c] = static_cast(lerp(top, bottom, yf)); - } - dst.set_pixel(x, y, pixel); - } - } - } - - // Bicubic resize function - // part of image will be cropped if the aspect ratio is different - static void resize_bicubic(const clip_image_u8 & img, clip_image_u8 & dst, int target_width, int target_height) { - const auto img_size = img.get_size(); - const int nx = img_size.width; - const int ny = img_size.height; - - dst.set_size({target_width, target_height}, false); - - if (img.is_placeholder()) { - // no-op for placeholder image, just set the size and return - return; - } - - float Cc; - float C[5] = {}; - float d0, d2, d3, a0, a1, a2, a3; - int i, j, k, jj; - int x, y; - float dx, dy; - float tx, ty; - - tx = (float)nx / (float)target_width; - ty = (float)ny / (float)target_height; - - // Bicubic interpolation; adapted from ViT.cpp, inspired from : - // -> https://github.com/yglukhov/bicubic-interpolation-image-processing/blob/master/libimage.c#L36 - // -> https://en.wikipedia.org/wiki/Bicubic_interpolation - - for (i = 0; i < target_height; i++) { - for (j = 0; j < target_width; j++) { - x = (int)(tx * j); - y = (int)(ty * i); - - dx = tx * j - x; - dy = ty * i - y; - - std::array pixel; - for (k = 0; k < 3; k++) { - for (jj = 0; jj <= 3; jj++) { - d0 = img.get_pixel(clip(x - 1, 0, nx - 1), clip(y - 1 + jj, 0, ny - 1))[k] - img.get_pixel(clip(x, 0, nx - 1), clip(y - 1 + jj, 0, ny - 1))[k]; - d2 = img.get_pixel(clip(x + 1, 0, nx - 1), clip(y - 1 + jj, 0, ny - 1))[k] - img.get_pixel(clip(x, 0, nx - 1), clip(y - 1 + jj, 0, ny - 1))[k]; - d3 = img.get_pixel(clip(x + 2, 0, nx - 1), clip(y - 1 + jj, 0, ny - 1))[k] - img.get_pixel(clip(x, 0, nx - 1), clip(y - 1 + jj, 0, ny - 1))[k]; - a0 = img.get_pixel(clip(x, 0, nx - 1), clip(y - 1 + jj, 0, ny - 1))[k]; - - a1 = -1.0 / 3 * d0 + d2 - 1.0 / 6 * d3; - a2 = 1.0 / 2 * d0 + 1.0 / 2 * d2; - a3 = -1.0 / 6 * d0 - 1.0 / 2 * d2 + 1.0 / 6 * d3; - - C[jj] = a0 + a1 * dx + a2 * dx * dx + a3 * dx * dx * dx; - - d0 = C[0] - C[1]; - d2 = C[2] - C[1]; - d3 = C[3] - C[1]; - a0 = C[1]; - a1 = -1.0 / 3 * d0 + d2 - 1.0 / 6 * d3; - a2 = 1.0 / 2 * d0 + 1.0 / 2 * d2; - a3 = -1.0 / 6 * d0 - 1.0 / 2 * d2 + 1.0 / 6 * d3; - Cc = a0 + a1 * dy + a2 * dy * dy + a3 * dy * dy * dy; - - const uint8_t Cc2 = std::min(std::max(std::round(Cc), 0.0f), 255.0f); - pixel[k] = Cc2; - } - } - dst.set_pixel(j, i, pixel); - } - } - } - - // Pillow-compatible separable resampling (Bicubic and Lanczos) + // Pillow-compatible separable resampling (Bilinear, Bicubic and Lanczos) // Adapted from https://github.com/python-pillow/Pillow/blob/main/src/libImaging/Resample.c // // Key properties: // 1. Separable filtering: horizontal pass followed by vertical pass // 2. Pre-computes normalized filter coefficients for each output pixel // 3. Fixed-point integer arithmetic (22 fractional bits) for speed and determinism - static bool resize_bicubic_pillow(const clip_image_u8 & img, clip_image_u8 & dst, int target_width, int target_height) { - return resize_pillow(img, dst, target_width, target_height, /*use_lanczos=*/false); - } - - // Lanczos-3 (support radius 3), matches Pillow's Image.LANCZOS - static bool resize_lanczos_pillow(const clip_image_u8 & img, clip_image_u8 & dst, int target_width, int target_height) { - return resize_pillow(img, dst, target_width, target_height, /*use_lanczos=*/true); - } - static bool resize_pillow( const clip_image_u8 & img, clip_image_u8 & dst, int target_width, int target_height, - bool use_lanczos) { + resize_algo algo) { // Fixed-point precision: 22 bits = 32 (int32_t) - 8 (uint8_t pixels) - 2 (headroom for accumulation) // This allows encoding fractional weights as integers: weight * 2^22 const int PRECISION_BITS = 32 - 8 - 2; - // Resample filter: Lanczos-3 (support [-3, 3]) or bicubic with a = -0.5 (support [-2, 2]) - // Note: GGML/PyTorch bicubic uses a = -0.75, Pillow uses a = -0.5 + // Filter support radius + double filter_support; + switch (algo) { + case RESIZE_ALGO_BILINEAR: filter_support = 1.0; break; + case RESIZE_ALGO_BICUBIC: filter_support = 2.0; break; + case RESIZE_ALGO_LANCZOS: filter_support = 3.0; break; + default: + throw std::runtime_error("Unsupported resize algorithm"); + } + // Returns filter weight for distance x from pixel center - auto resample_filter = [use_lanczos](double x) -> double { - if (use_lanczos) { + // Note: for bicubic, Pillow uses a = -0.5 while GGML/PyTorch use a = -0.75 + auto resample_filter = [algo](double x) -> double { + if (algo == RESIZE_ALGO_LANCZOS) { if (-3.0 <= x && x < 3.0) { auto sinc = [](double v) { if (v == 0.0) { @@ -383,10 +238,15 @@ private: return 0.0; } - constexpr double a = -0.5; if (x < 0.0) { x = -x; } + + if (algo == RESIZE_ALGO_BILINEAR) { + return x < 1.0 ? 1.0 - x : 0.0; + } + + constexpr double a = -0.5; if (x < 1.0) { return ((a + 2.0) * x - (a + 3.0)) * x * x + 1; } @@ -396,9 +256,6 @@ private: return 0.0; // Zero outside [-2, 2] }; - // Filter support radius: 2 for bicubic, 3 for lanczos - const double filter_support = use_lanczos ? 3.0 : 2.0; - // Clipping function for 8-bit values auto clip8 = [](int val) -> uint8_t { if (val < 0) return 0; @@ -493,100 +350,92 @@ private: const double fxp_scale = std::ldexp(1.0, PRECISION_BITS); // 1.0 * 2^PRECISION_BITS for (int i = 0; i < outSize * ksize; i++) { - if (use_lanczos) { - // Pillow adds +/- 0.5 then truncates toward zero; std::round would round twice - const double rounded = pre_weights[i] * fxp_scale + (pre_weights[i] < 0 ? -0.5 : 0.5); - weights[i] = static_cast(rounded); - continue; - } - double tmp_val = pre_weights[i] * fxp_scale; - if (pre_weights[i] < 0) { - tmp_val -= 0.5; - } else { - tmp_val += 0.5; - } - tmp_val = std::round(tmp_val); - tmp_val = std::clamp(tmp_val, - static_cast(std::numeric_limits::min()), - static_cast(std::numeric_limits::max())); - weights[i] = static_cast(tmp_val); + // Pillow adds +/- 0.5 then truncates toward zero; std::round would round twice + const double rounded = pre_weights[i] * fxp_scale + (pre_weights[i] < 0 ? -0.5 : 0.5); + weights[i] = static_cast(rounded); } return ksize; }; // Horizontal resampling pass - // Resizes width from imIn to out_nx, preserving height - auto resample_horizontal = [&](const clip_image_u8 & imIn, clip_image_u8 & imOut, + // Resizes width from src to out_nx, preserving height + auto resample_horizontal = [&](const uint8_t * src, int in_nx, int in_ny, int out_nx, int ksize, const std::vector & bounds, const std::vector & weights) { - const int in_ny = imIn.get_size().height; - imOut.set_size({out_nx, in_ny}, false); + std::vector out((size_t) out_nx * in_ny * 3); // Process each row independently for (int yy = 0; yy < in_ny; yy++) { + const uint8_t * src_row = src + (size_t) yy * in_nx * 3; + uint8_t * dst_row = out.data() + (size_t) yy * out_nx * 3; + // For each output pixel in this row for (int xx = 0; xx < out_nx; xx++) { - // Get the range of input pixels and filter coefficients - int xmin = bounds[xx * 2 + 0]; // First input pixel index - int xcnt = bounds[xx * 2 + 1]; // Number of input pixels + const int xmin = bounds[xx * 2 + 0]; // First input pixel index + const int xcnt = bounds[xx * 2 + 1]; // Number of input pixels + const int32_t * k = &weights[xx * ksize]; + const uint8_t * p = src_row + (size_t) xmin * 3; - // Initialize accumulators for RGB channels with rounding bias (0.5 in fixed-point) + // Accumulators for RGB channels, with rounding bias (0.5 in fixed-point) int32_t ss0 = 1 << (PRECISION_BITS - 1); int32_t ss1 = 1 << (PRECISION_BITS - 1); int32_t ss2 = 1 << (PRECISION_BITS - 1); // Convolve: sum weighted input pixels for (int x = 0; x < xcnt; x++) { - const auto src_px = imIn.get_pixel(x + xmin, yy); - ss0 += src_px[0] * weights[xx * ksize + x]; // R channel - ss1 += src_px[1] * weights[xx * ksize + x]; // G channel - ss2 += src_px[2] * weights[xx * ksize + x]; // B channel + ss0 += p[0] * k[x]; + ss1 += p[1] * k[x]; + ss2 += p[2] * k[x]; + p += 3; } // Convert back from fixed-point (divide by 2^PRECISION_BITS) and clamp to [0,255] - imOut.set_pixel(xx, yy, {clip8(ss0 >> PRECISION_BITS), - clip8(ss1 >> PRECISION_BITS), - clip8(ss2 >> PRECISION_BITS)}); + dst_row[xx * 3 + 0] = clip8(ss0 >> PRECISION_BITS); + dst_row[xx * 3 + 1] = clip8(ss1 >> PRECISION_BITS); + dst_row[xx * 3 + 2] = clip8(ss2 >> PRECISION_BITS); } } + + return out; }; // Vertical resampling pass - // Resizes height from imIn to out_ny, preserving width - auto resample_vertical = [&](const clip_image_u8 & imIn, clip_image_u8 & imOut, + // Resizes height from src to out_ny, preserving width + // Accumulates whole rows at once (contiguous access, auto-vectorizes well) + auto resample_vertical = [&](const uint8_t * src, int in_nx, int out_ny, int ksize, const std::vector & bounds, const std::vector & weight) { - const int in_nx = imIn.get_size().width; - imOut.set_size({in_nx, out_ny}, false); + const size_t row_elems = (size_t) in_nx * 3; + std::vector out(row_elems * out_ny); + std::vector acc(row_elems); // For each output row for (int yy = 0; yy < out_ny; yy++) { - // Get the range of input rows and filter coefficients - int ymin = bounds[yy * 2 + 0]; // First input row index - int ycnt = bounds[yy * 2 + 1]; // Number of input rows + const int ymin = bounds[yy * 2 + 0]; // First input row index + const int ycnt = bounds[yy * 2 + 1]; // Number of input rows + const int32_t * k = &weight[yy * ksize]; - // Process each column in this output row - for (int xx = 0; xx < in_nx; xx++) { - // Initialize accumulators for RGB channels with rounding bias - int32_t ss0 = 1 << (PRECISION_BITS - 1); - int32_t ss1 = 1 << (PRECISION_BITS - 1); - int32_t ss2 = 1 << (PRECISION_BITS - 1); + // Rounding bias (0.5 in fixed-point) + std::fill(acc.begin(), acc.end(), 1 << (PRECISION_BITS - 1)); - // Convolve: sum weighted input pixels vertically - for (int y = 0; y < ycnt; y++) { - const auto src_px = imIn.get_pixel(xx, y + ymin); - ss0 += src_px[0] * weight[yy * ksize + y]; // R channel - ss1 += src_px[1] * weight[yy * ksize + y]; // G channel - ss2 += src_px[2] * weight[yy * ksize + y]; // B channel + // Convolve: accumulate each weighted input row + for (int y = 0; y < ycnt; y++) { + const uint8_t * src_row = src + (size_t) (ymin + y) * row_elems; + const int32_t w = k[y]; + for (size_t i = 0; i < row_elems; i++) { + acc[i] += src_row[i] * w; } + } - // Convert back from fixed-point and clamp to [0,255] - imOut.set_pixel(xx, yy, {clip8(ss0 >> PRECISION_BITS), - clip8(ss1 >> PRECISION_BITS), - clip8(ss2 >> PRECISION_BITS)}); + // Convert back from fixed-point and clamp to [0,255] + uint8_t * dst_row = out.data() + (size_t) yy * row_elems; + for (size_t i = 0; i < row_elems; i++) { + dst_row[i] = clip8(acc[i] >> PRECISION_BITS); } } + + return out; }; // Main resampling logic using separable two-pass approach @@ -610,36 +459,25 @@ private: } // Perform two-pass resampling + const uint8_t * src = img.get_ro_buf().data(); if (need_horizontal && need_vertical) { - // Both horizontal and vertical - clip_image_u8 temp; - resample_horizontal(img, temp, target_width, ksize_horiz, bounds_horiz, weights_horiz); - resample_vertical(temp, dst, target_height, ksize_vert, bounds_vert, weights_vert); + auto temp = resample_horizontal(src, src_width, src_height, target_width, ksize_horiz, bounds_horiz, weights_horiz); + dst.set_size({target_width, target_height}, false); + dst.cpy_buf(resample_vertical(temp.data(), target_width, target_height, ksize_vert, bounds_vert, weights_vert)); } else if (need_horizontal) { - // Only horizontal - resample_horizontal(img, dst, target_width, ksize_horiz, bounds_horiz, weights_horiz); + dst.set_size({target_width, src_height}, false); + dst.cpy_buf(resample_horizontal(src, src_width, src_height, target_width, ksize_horiz, bounds_horiz, weights_horiz)); } else if (need_vertical) { - // Only vertical - resample_vertical(img, dst, target_height, ksize_vert, bounds_vert, weights_vert); + dst.set_size({src_width, target_height}, false); + dst.cpy_buf(resample_vertical(src, src_width, target_height, ksize_vert, bounds_vert, weights_vert)); } else { // No resizing needed - direct copy - dst.set_size(img.get_size(), img.is_placeholder()); - if (!img.is_placeholder()) { - dst.cpy_buf(img.get_ro_buf()); - } + dst.set_size(img.get_size(), false); + dst.cpy_buf(img.get_ro_buf()); } return true; } - - static inline int clip(int x, int lower, int upper) { - return std::max(lower, std::min(x, upper)); - } - - // Linear interpolation between two points - static inline float lerp(float s, float e, float t) { - return s + (e - s) * t; - } }; @@ -1264,7 +1102,7 @@ mtmd_image_preproc_out mtmd_image_preprocessor_deepseekocr::preprocess(const cli clip_image_u8 padded; img_tool::resize(img, padded, { base_size, base_size }, - RESIZE_ALGO_BICUBIC_PILLOW, + RESIZE_ALGO_BICUBIC, PAD_NEAREST, hparams.image_pad_color); output.append_overview(hparams, padded, true); @@ -1280,7 +1118,7 @@ mtmd_image_preproc_out mtmd_image_preprocessor_deepseekocr::preprocess(const cli grid_h = grid.height; clip_image_u8 refined; - img_tool::resize(img, refined, { tile_size * grid_w, tile_size * grid_h }, RESIZE_ALGO_BICUBIC_PILLOW, + img_tool::resize(img, refined, { tile_size * grid_w, tile_size * grid_h }, RESIZE_ALGO_BICUBIC, PAD_NONE); for (int row = 0; row < grid_h; row++) { From 4a08fa29705b8177e332b134306566c2c4b95902 Mon Sep 17 00:00:00 2001 From: Xuan-Son Nguyen Date: Sun, 23 Aug 2026 18:38:51 +0200 Subject: [PATCH 15/28] test: move tools/parser to tests (#27548) --- docs/autoparser.md | 14 +- skills/add-new-model/SKILL.md | 2 +- tests/CMakeLists.txt | 2 + .../test-chat-analysis.cpp | 14 +- tests/test-chat-auto-parser.cpp | 445 ++++++++++++++++- tests/test-chat-template.cpp | 2 + tools/CMakeLists.txt | 1 - tools/parser/CMakeLists.txt | 20 - tools/parser/debug-template-parser.cpp | 469 ------------------ 9 files changed, 465 insertions(+), 504 deletions(-) rename tools/parser/template-analysis.cpp => tests/test-chat-analysis.cpp (98%) delete mode 100644 tools/parser/CMakeLists.txt delete mode 100644 tools/parser/debug-template-parser.cpp diff --git a/docs/autoparser.md b/docs/autoparser.md index 33ede1a22..b5e32621d 100644 --- a/docs/autoparser.md +++ b/docs/autoparser.md @@ -443,21 +443,21 @@ Each returned parser is wrapped by `wrap_for_generation_prompt()`, which prepend | | `wrap_for_generation_prompt()`, string helpers | | `common/chat-peg-parser.h/cpp` | `common_chat_peg_builder`, `common_chat_peg_mapper`, and helpers | | `common/chat.cpp` | Entry point: `common_chat_templates_apply_jinja()` | -| `tools/parser/debug-template-parser.cpp` | Debug tool for template analysis | -| `tools/parser/template-analysis.cpp` | Template analysis tool | +| `tests/test-chat-auto-parser.cpp` | Auto-parser unit tests; also a debug tool when given a template path | +| `tests/test-chat-analysis.cpp` | Template differential analysis debug tool | ## Testing & Debugging ### Debug Tools -**Template Debugger**: `tools/parser/debug-template-parser.cpp` +**Template Debugger**: `tests/test-chat-auto-parser.cpp` -- Usage: `./bin/llama-debug-template-parser path/to/template.jinja` +- Usage: `./bin/test-chat-auto-parser path/to/template.jinja` (without a path, it runs the automated tests) - Shows detected format, markers, generated parser, and GBNF grammar -**Template Analysis**: `tools/parser/template-analysis.cpp` +**Template Analysis**: `tests/test-chat-analysis.cpp` -- Usage: `./bin/llama-template-analysis path/to/template.jinja` +- Usage: `./bin/test-chat-analysis --template-file path/to/template.jinja` (without arguments, it runs on all templates from the test suite) **Debug Logging**: Enable with `LLAMA_ARG_LOG_VERBOSITY=2` @@ -519,7 +519,7 @@ The following templates have active tests in `tests/test-chat.cpp`: To support a new template format: -1. **If it follows standard patterns** — The auto-parser should detect it automatically. Run `llama-debug-template-parser` to verify markers are correctly extracted. +1. **If it follows standard patterns** — The auto-parser should detect it automatically. Run `test-chat-auto-parser ` to verify markers are correctly extracted. 2. **If differential analysis extracts incorrect markers** — Add a workaround lambda to the `workarounds` vector in `common/chat-diff-analyzer.cpp`. Inspect the template source for a unique identifying substring. 3. **If it needs fundamentally different handling** — Add a dedicated handler function in `chat.cpp` before the auto-parser block (as done for GPT-OSS, Functionary v3.2, and Ministral). diff --git a/skills/add-new-model/SKILL.md b/skills/add-new-model/SKILL.md index f76d1abfd..710a1ebb4 100644 --- a/skills/add-new-model/SKILL.md +++ b/skills/add-new-model/SKILL.md @@ -66,7 +66,7 @@ These recur often enough in review comments on past add-model PRs that they're w - Optional hparams that are genuinely absent from some configs (e.g. a shared-expert count) should be read with an explicit optional/fallback accessor, not assumed present. - Hparams that are actually load-bearing (the model produces wrong output or crashes without them, e.g. `sliding_window_pattern`, norm-eps) must hard-error if missing, not silently fall back to a default. - Don't bake a default chat template into the C++ binary - inject it into the GGUF at conversion time instead, since one `llm_arch` can be reused by multiple fine-tunes with different templates, and a baked-in C++ default fails silently for those. -- Before writing a dedicated tool-call/output parser, check whether the existing autoparser already handles the template (`llama-debug-template-parser ` shows what it detects). +- Before writing a dedicated tool-call/output parser, check whether the existing autoparser already handles the template (`test-chat-auto-parser ` shows what it detects). - Marking a custom EOS/closing-tag token as `eot` at conversion time isn't always sufficient - in long/agentic generations a model can emit the closing sequence as literal text instead of the token, so generation never stops on EOG and raw text leaks past the parser. Verify this case, not just the token path. - If reusing or aliasing an existing pre-tokenizer for convenience, justify and test that choice explicitly - silent reuse is an easy source of subtle tokenizer bugs. - Watch for excessive graph splits caused by building per-layer view/index tensors inside the layer loop - hoist tensors that don't vary per layer out of the loop (relevant if you hit `GGML_SCHED_MAX_SPLIT_INPUTS`). diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index cb6ae2970..b9f9d4b78 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -244,6 +244,8 @@ llama_build_and_test(test-jinja.cpp) llama_test(test-jinja NAME test-jinja-py ARGS -py LABEL python) llama_build_and_test(test-chat-auto-parser.cpp WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}) llama_build_and_test(test-chat-template.cpp) +# debug tool for chat template differential analysis (not registered as a test, run it manually) +llama_build(test-chat-analysis.cpp) llama_build_and_test(test-log.cpp) llama_build_and_test( test-peg-parser.cpp diff --git a/tools/parser/template-analysis.cpp b/tests/test-chat-analysis.cpp similarity index 98% rename from tools/parser/template-analysis.cpp rename to tests/test-chat-analysis.cpp index 11225bd8c..42ad7a072 100644 --- a/tools/parser/template-analysis.cpp +++ b/tests/test-chat-analysis.cpp @@ -84,11 +84,12 @@ static std::string read_file(const std::string & path) { } static void print_usage(const char * program_name) { - LOG_ERR("Usage: %s [options]\n", program_name); + LOG_ERR("Debug the auto-parser's differential analysis: render a template with/without tools, reasoning, etc. and show the diffs.\n"); + LOG_ERR("\nUsage: %s [options]\n", program_name); LOG_ERR("\nOptions:\n"); LOG_ERR(" --template Analyze specific template from test suite (e.g., 'deepseek' or 'DeepSeek-V3.1')\n"); LOG_ERR(" --template-file Analyze custom template file\n"); - LOG_ERR(" --all Analyze all templates from test suite\n"); + LOG_ERR(" --all Analyze all templates from test suite (default when no arguments are given)\n"); LOG_ERR("\nExamples:\n"); LOG_ERR(" %s --all\n", program_name); LOG_ERR(" %s --template deepseek\n", program_name); @@ -97,14 +98,17 @@ static void print_usage(const char * program_name) { static bool parse_options(int argc, char ** argv, analysis_options & opts) { if (argc < 2) { - print_usage(argv[0]); - return false; + // default mode: analyze all templates from the test suite + opts.analyze_all = true; } for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; - if (arg == "--all") { + if (arg == "-h" || arg == "--help") { + print_usage(argv[0]); + return false; + } else if (arg == "--all") { opts.analyze_all = true; } else if (arg == "--template") { if (i + 1 >= argc) { diff --git a/tests/test-chat-auto-parser.cpp b/tests/test-chat-auto-parser.cpp index 2209dcac8..5aa948251 100644 --- a/tests/test-chat-auto-parser.cpp +++ b/tests/test-chat-auto-parser.cpp @@ -2,11 +2,18 @@ #include "chat-auto-parser.h" #include "chat-peg-parser.h" #include "chat.h" +#include "gguf.h" +#include "jinja/runtime.h" +#include "log.h" #include "peg-parser.h" #include "testing.h" +#include +#include #include #include +#include +#include #include #include @@ -94,11 +101,447 @@ static void test_bailing_v3_tool_format(testing & t); static void test_role_markers_all_templates(testing & t); +static json build_tools_definition(); + +// +// debug mode: analyze a single template and dump the generated parser and grammar +// + +enum class output_mode { + ANALYSIS, // Only output analysis results (default) + TEMPLATE, // Only output rendered template + BOTH // Output both +}; + +enum class input_message_type { + NONE, // Don't render any message scenarios (only analysis) + CONTENT_ONLY, // Simple assistant message with content + REASONING_CONTENT, // Message with reasoning_content + content + TOOL_CALL_ONLY, // Message with tool_calls only + CONTENT_TOOL_CALL, // Message with content + tool_calls + REASONING_TOOL_CALL, // Message with reasoning_content + tool_calls + CONTENT_FAKE_TOOL_CALL, // Message with content but no actual tool_calls (for testing) + ALL // Render all scenarios +}; + +struct debug_options { + std::string template_path; + bool with_tools = true; + bool generation_prompt = true; + bool enable_reasoning = true; + bool debug_jinja = false; + bool force_tool_call = false; + bool parallel_tool_calls = true; + output_mode mode = output_mode::BOTH; + input_message_type input_message = input_message_type::NONE; +}; + +static std::string read_file(const std::string & path) { + std::ifstream fin(path, std::ios::binary); + if (!fin.is_open()) { + throw std::runtime_error("Could not open file: " + path); + } + std::ostringstream buf; + buf << fin.rdbuf(); + return buf.str(); +} + +static std::string read_gguf_chat_template(const std::string & path) { + struct gguf_init_params params = { /*no_alloc =*/true, // We only need metadata, not tensor data + /*ctx=*/nullptr }; + + struct gguf_context * ctx = gguf_init_from_file(path.c_str(), params); + if (ctx == nullptr) { + throw std::runtime_error("Could not open GGUF file: " + path); + } + + const char * key = "tokenizer.chat_template"; + int64_t key_id = gguf_find_key(ctx, key); + + if (key_id == -1) { + gguf_free(ctx); + throw std::runtime_error("GGUF file does not contain chat template key: " + std::string(key)); + } + + const char * template_str = gguf_get_val_str(ctx, key_id); + if (template_str == nullptr) { + gguf_free(ctx); + throw std::runtime_error("GGUF file contains chat template key but value is null"); + } + + std::string result = template_str; + gguf_free(ctx); + return result; +} + +static void print_usage(const char * program_name) { + LOG_ERR("Test the chat template auto-parser; also usable as a debug tool that shows the generated PEG parser, GBNF grammar and triggers for a given template.\n"); + LOG_ERR("\nUsage: %s [filter_regex] run the automated tests (default)\n", program_name); + LOG_ERR(" %s [options] debug a single template\n", program_name); + LOG_ERR("\nDebug mode options:\n"); + LOG_ERR(" --no-tools Disable tool definitions\n"); + LOG_ERR(" --force-tool-call Set tool calls to forced\n"); + LOG_ERR(" --parallel-tool-calls=0|1 Set parallel_tool_calls (default: 1)\n"); + LOG_ERR(" --generation-prompt=0|1 Set add_generation_prompt (default: 1)\n"); + LOG_ERR(" --enable-reasoning=0|1 Enable reasoning parsing (default: 1)\n"); + LOG_ERR(" --output=MODE Output mode: analysis, template, both (default: both)\n"); + LOG_ERR(" --debug-jinja Enable Jinja fine-grained debug\n"); + LOG_ERR(" --input-message=TYPE Message type to render:\n"); + LOG_ERR(" content_only, reasoning_content, tool_call_only,\n"); + LOG_ERR(" content_tool_call, reasoning_tool_call,\n"); + LOG_ERR(" content_fake_tool_call, all\n"); + LOG_ERR("\nExamples:\n"); + LOG_ERR(" %s template.jinja --input-message=all --generation-prompt=1\n", program_name); + LOG_ERR(" %s template.jinja --output=template --input-message=tool_call_only\n", program_name); +} + +static bool parse_bool_option(const std::string & value) { + return value == "1" || value == "true" || value == "yes"; +} + +static bool parse_debug_options(int argc, char ** argv, debug_options & opts) { + opts.template_path = argv[1]; + + for (int i = 2; i < argc; ++i) { + std::string arg = argv[i]; + + if (arg == "--force-tool-call") { + opts.force_tool_call = true; + } else if (arg == "--debug-jinja") { + opts.debug_jinja = true; + } else if (arg == "--no-tools") { + opts.with_tools = false; + } else if (arg.rfind("--parallel-tool-calls=", 0) == 0) { + opts.parallel_tool_calls = parse_bool_option(arg.substr(22)); + } else if (arg.rfind("--generation-prompt=", 0) == 0) { + opts.generation_prompt = parse_bool_option(arg.substr(20)); + } else if (arg.rfind("--enable-reasoning=", 0) == 0) { + opts.enable_reasoning = parse_bool_option(arg.substr(19)); + } else if (arg.rfind("--output=", 0) == 0) { + std::string mode = arg.substr(9); + if (mode == "analysis") { + opts.mode = output_mode::ANALYSIS; + } else if (mode == "template") { + opts.mode = output_mode::TEMPLATE; + } else if (mode == "both") { + opts.mode = output_mode::BOTH; + } else { + LOG_ERR("Unknown output mode: %s\n", mode.c_str()); + return false; + } + } else if (arg.rfind("--input-message=", 0) == 0) { + std::string type = arg.substr(16); + if (type == "content_only") { + opts.input_message = input_message_type::CONTENT_ONLY; + } else if (type == "reasoning_content") { + opts.input_message = input_message_type::REASONING_CONTENT; + } else if (type == "tool_call_only") { + opts.input_message = input_message_type::TOOL_CALL_ONLY; + } else if (type == "content_tool_call") { + opts.input_message = input_message_type::CONTENT_TOOL_CALL; + } else if (type == "reasoning_tool_call") { + opts.input_message = input_message_type::REASONING_TOOL_CALL; + } else if (type == "content_fake_tool_call") { + opts.input_message = input_message_type::CONTENT_FAKE_TOOL_CALL; + } else if (type == "all") { + opts.input_message = input_message_type::ALL; + } else { + LOG_ERR("Unknown input message type: %s\n", type.c_str()); + return false; + } + } else { + LOG_ERR("Unknown option: %s\n", arg.c_str()); + print_usage(argv[0]); + return false; + } + } + + return true; +} + +static json build_debug_user_message() { + return json{ + { "role", "user" }, + { "content", "Hello, please help me with a task." } + }; +} + +static json build_content_only_message() { + return json{ + { "role", "assistant" }, + { "content", "Hello! I'm here to help you with your task." } + }; +} + +static json build_reasoning_content_message() { + return json{ + { "role", "assistant" }, + { "content", "Hello! I'm here to help you with your task." }, + { "reasoning_content", "The user is greeting me and asking for help. I should respond politely." } + }; +} + +static json build_tool_call_only_message() { + return json{ + { "role", "assistant" }, + { "content", nullptr }, + { "tool_calls", + json::array({ json{ + { "type", "function" }, + { "function", json{ { "name", "test_function_name" }, + { "arguments", json::object({ { "param1", "value1" }, { "param2", "value2" } }) } } }, + { "id", "123456789" } } }) } + }; +} + +static json build_content_tool_call_message() { + return json{ + { "role", "assistant" }, + { "content", "I'll help you by calling a function." }, + { "tool_calls", + json::array({ json{ + { "type", "function" }, + { "function", + json{ { "name", "test_function_name" }, + { "arguments", json::object({ { "param1", "value1" }, { "param2", "value2" } }) } } } } }) } + }; +} + +static json build_reasoning_tool_call_message() { + return json{ + { "role", "assistant" }, + { "content", nullptr }, + { "reasoning_content", "I need to call a function to help with this task." }, + { "tool_calls", + json::array({ json{ + { "type", "function" }, + { "function", + json{ { "name", "test_function_name" }, + { "arguments", json::object({ { "param1", "value1" }, { "param2", "value2" } }) } } } } }) } + }; +} + +static json build_content_fake_tool_call_message() { + // This message has content but NO tool_calls field + // It's used to test if a template renders tool definitions but not tool calls + return json{ + { "role", "assistant" }, + { "content", "I'll help you by calling a function." } + }; +} + +static void render_scenario(const common_chat_template & tmpl, + const std::string & scenario_name, + const json & messages, + const json & tools, + bool add_generation_prompt, + bool enable_thinking) { + LOG_ERR("\n=== Scenario: %s ===\n", scenario_name.c_str()); + LOG_ERR("add_generation_prompt: %s, enable_thinking: %s\n", add_generation_prompt ? "true" : "false", + enable_thinking ? "true" : "false"); + + // When add_generation_prompt is true, add a trailing user message to trigger the prompt + json final_messages = messages; + if (add_generation_prompt && !messages.empty() && messages.back().value("role", "") == "assistant") { + final_messages.push_back(json{ + { "role", "user" }, + { "content", "Now please continue with another response." } + }); + } + + LOG_ERR("Messages:\n%s\n", final_messages.dump(2).c_str()); + + try { + generation_params inputs; + inputs.messages = final_messages; + inputs.add_generation_prompt = add_generation_prompt; + inputs.extra_context["enable_thinking"] = enable_thinking; + + if (!tools.is_null() && tools.is_array() && !tools.empty()) { + inputs.tools = tools; + } + + std::string output = common_chat_template_direct_apply(tmpl, inputs); + + LOG_ERR("\n--- Rendered Output ---\n"); + LOG_ERR("%s\n", output.c_str()); + LOG_ERR("--- End Output (length: %zu) ---\n", output.length()); + } catch (const std::exception & e) { + LOG_ERR("Rendering failed: %s\n", e.what()); + } +} + +static void render_all_scenarios(const common_chat_template & tmpl, + const json & tools, + bool add_generation_prompt, + bool enable_thinking, + input_message_type message_type) { + json user_msg = build_debug_user_message(); + + auto render_if = [&](input_message_type type, const std::string & name, const json & assistant_msg) { + if (message_type == input_message_type::ALL || message_type == type) { + json messages = json::array({ user_msg, assistant_msg }); + render_scenario(tmpl, name, messages, tools, add_generation_prompt, enable_thinking); + } + }; + + render_if(input_message_type::CONTENT_ONLY, "content_only", build_content_only_message()); + render_if(input_message_type::REASONING_CONTENT, "reasoning_content", build_reasoning_content_message()); + render_if(input_message_type::TOOL_CALL_ONLY, "tool_call_only", build_tool_call_only_message()); + render_if(input_message_type::CONTENT_TOOL_CALL, "content_tool_call", build_content_tool_call_message()); + render_if(input_message_type::REASONING_TOOL_CALL, "reasoning_tool_call", build_reasoning_tool_call_message()); + render_if(input_message_type::CONTENT_FAKE_TOOL_CALL, "content_fake_tool_call", + build_content_fake_tool_call_message()); + + // Also render with add_generation_prompt=true to show the prompt ending + if (message_type == input_message_type::ALL) { + LOG_ERR("\n\n=== Generation Prompt Scenarios (add_generation_prompt=true) ===\n"); + + json prompt_messages = json::array({ user_msg }); + render_scenario(tmpl, "generation_prompt_only", prompt_messages, tools, true, enable_thinking); + + // With enable_thinking toggled + render_scenario(tmpl, "generation_prompt_thinking_disabled", prompt_messages, tools, true, false); + } +} + +static generation_params prepare_debug_params(const debug_options & opts, const json & tools) { + generation_params params; + params.messages = json::array({ build_debug_user_message() }); + params.reasoning_format = opts.enable_reasoning ? COMMON_REASONING_FORMAT_DEEPSEEK : COMMON_REASONING_FORMAT_NONE; + params.enable_thinking = opts.enable_reasoning; + params.add_generation_prompt = opts.generation_prompt; + + if (opts.with_tools) { + params.tools = tools; + params.tool_choice = opts.force_tool_call ? COMMON_CHAT_TOOL_CHOICE_REQUIRED : COMMON_CHAT_TOOL_CHOICE_AUTO; + } else { + params.tools = json(); + params.tool_choice = COMMON_CHAT_TOOL_CHOICE_NONE; + } + params.parallel_tool_calls = opts.parallel_tool_calls; + return params; +} + +static int debug_single_template(const debug_options & opts) { + std::string template_source; + try { + // Check if the file is a GGUF file + if (opts.template_path.size() >= 5 && + opts.template_path.compare(opts.template_path.size() - 5, 5, ".gguf") == 0) { + template_source = read_gguf_chat_template(opts.template_path); + } else { + template_source = read_file(opts.template_path); + } + } catch (const std::exception & e) { + LOG_ERR("Error reading template: %s\n", e.what()); + return 1; + } + + LOG_ERR("Analyzing template: %s\n", opts.template_path.c_str()); + LOG_ERR("Options: with_tools=%s, generation_prompt=%s, enable_reasoning=%s\n", opts.with_tools ? "true" : "false", + opts.generation_prompt ? "true" : "false", opts.enable_reasoning ? "true" : "false"); + + try { + common_chat_template chat_template(template_source, "", ""); + + json tools = opts.with_tools ? build_tools_definition() : json(); + + generation_params params = prepare_debug_params(opts, tools); + common_chat_params parser_data; + if (std::optional spec_tmpl = + common_chat_try_specialized_template(chat_template, template_source, params)) { + LOG_ERR("\n"); + LOG_ERR("This template uses a specialized parser, analysis results will not be available.\n"); + parser_data = *spec_tmpl; + } else { + // Render template scenarios if requested + if (opts.input_message != input_message_type::NONE && + (opts.mode == output_mode::TEMPLATE || opts.mode == output_mode::BOTH)) { + LOG_ERR("\n"); + LOG_ERR("================================================================================\n"); + LOG_ERR(" TEMPLATE RENDERING OUTPUT\n"); + LOG_ERR("================================================================================\n"); + + render_all_scenarios(chat_template, tools, opts.generation_prompt, opts.enable_reasoning, + opts.input_message); + } + + // Output analysis if requested + if (opts.mode == output_mode::ANALYSIS || opts.mode == output_mode::BOTH) { + LOG_ERR("\n"); + LOG_ERR("================================================================================\n"); + LOG_ERR(" TEMPLATE ANALYSIS\n"); + LOG_ERR("================================================================================\n"); + + struct autoparser analysis; + analysis.analyze_template(chat_template); + + // Generate Parser + parser_data = peg_generator::generate_parser(chat_template, params, analysis); + } + } + + if (!std::empty(parser_data.parser)) { + LOG_ERR("\n=== Generated Parser ===\n"); + common_peg_arena arena; + arena.load(parser_data.parser); + LOG_ERR("%s\n", arena.dump(arena.root()).c_str()); + + LOG_ERR("\n=== Generated Grammar ===\n"); + LOG_ERR("%s\n", parser_data.grammar.c_str()); + + LOG_ERR("\n=== Generated Lazy Grammar ===\n"); + LOG_ERR("%d\n", parser_data.grammar_lazy); + + LOG_ERR("\n=== Generated Grammar Triggers ===\n"); + for (const common_grammar_trigger & cgt : parser_data.grammar_triggers) { + LOG_ERR("Token: %d | Type: %d | Value: %s\n", cgt.token, cgt.type, cgt.value.c_str()); + } + + LOG_ERR("\n=== Preserved Tokens ===\n"); + for (const std::string & token : parser_data.preserved_tokens) { + LOG_ERR(" '%s'\n", token.c_str()); + } + } + } catch (const std::exception & e) { + LOG_ERR("Analysis failed: %s\n", e.what()); + return 1; + } + + return 0; +} + int main(int argc, char * argv[]) { + if (argc > 1) { + std::string arg = argv[1]; + if (arg == "-h" || arg == "--help") { + common_log_set_verbosity_thold(99); + print_usage(argv[0]); + return 0; + } + + // debug mode: if the first argument is an existing file, analyze that template instead of running the automated tests + if (std::filesystem::is_regular_file(arg)) { + common_log_set_verbosity_thold(99); + + debug_options opts; + if (!parse_debug_options(argc, argv, opts)) { + return 1; + } + + if (opts.debug_jinja || std::getenv("LLAMA_DEBUG_JINJA") != nullptr) { + jinja::enable_debug(true); + } + + return debug_single_template(opts); + } + } + testing t(std::cout); t.verbose = true; - // usage: test-chat-auto-parser-helpers [filter_regex] + // usage: test-chat-auto-parser [filter_regex] if (argc > 1) { t.set_filter(argv[1]); diff --git a/tests/test-chat-template.cpp b/tests/test-chat-template.cpp index bcc574afe..a477180cd 100644 --- a/tests/test-chat-template.cpp +++ b/tests/test-chat-template.cpp @@ -28,6 +28,8 @@ static void run_multiple(const std::string& dir_path, bool stop_on_first_failure static void run_single(const std::string& contents, json input, bool use_common = false, bool dump_prog = false, const std::string & output_path = ""); static std::string HELP = R"( +Test the Jinja engine by rendering chat templates and comparing the output against expected results. + Usage: test-chat-template [OPTIONS] PATH_TO_TEMPLATE Options: -h, --help Show this help message and exit. diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 780df3266..37561563f 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -27,7 +27,6 @@ else() add_subdirectory(server) endif() add_subdirectory(tokenize) - add_subdirectory(parser) add_subdirectory(tts) add_subdirectory(mtmd) if (GGML_RPC) diff --git a/tools/parser/CMakeLists.txt b/tools/parser/CMakeLists.txt deleted file mode 100644 index a8df0e7e6..000000000 --- a/tools/parser/CMakeLists.txt +++ /dev/null @@ -1,20 +0,0 @@ -if (NOT WIN32 OR NOT BUILD_SHARED_LIBS) - # this tool is disabled on Windows when building with shared libraries because it uses internal functions not exported with LLAMA_API - set(TARGET llama-debug-template-parser) - add_executable(${TARGET} debug-template-parser.cpp) - target_link_libraries(${TARGET} PRIVATE llama-common llama ${CMAKE_THREAD_LIBS_INIT}) - target_compile_features(${TARGET} PRIVATE cxx_std_17) - - if(LLAMA_TOOLS_INSTALL) - install(TARGETS ${TARGET} RUNTIME) - endif() -endif() - -set(TARGET llama-template-analysis) -add_executable(${TARGET} template-analysis.cpp) -target_link_libraries(${TARGET} PRIVATE llama-common llama ${CMAKE_THREAD_LIBS_INIT}) -target_compile_features(${TARGET} PRIVATE cxx_std_17) - -if(LLAMA_TOOLS_INSTALL) - install(TARGETS ${TARGET} RUNTIME) -endif() diff --git a/tools/parser/debug-template-parser.cpp b/tools/parser/debug-template-parser.cpp deleted file mode 100644 index abe427022..000000000 --- a/tools/parser/debug-template-parser.cpp +++ /dev/null @@ -1,469 +0,0 @@ -#include "../src/llama-grammar.h" -#include "chat-auto-parser.h" -#include "chat.h" -#include "common.h" -#include "gguf.h" -#include "jinja/runtime.h" -#include "log.h" -#include "json.h" -#include "peg-parser.h" - -#include -#include -#include -#include -#include -#include - -using json = common_json; - -enum class output_mode { - ANALYSIS, // Only output analysis results (default) - TEMPLATE, // Only output rendered template - BOTH // Output both -}; - -enum class input_message_type { - NONE, // Don't render any message scenarios (only analysis) - CONTENT_ONLY, // Simple assistant message with content - REASONING_CONTENT, // Message with reasoning_content + content - TOOL_CALL_ONLY, // Message with tool_calls only - CONTENT_TOOL_CALL, // Message with content + tool_calls - REASONING_TOOL_CALL, // Message with reasoning_content + tool_calls - CONTENT_FAKE_TOOL_CALL, // Message with content but no actual tool_calls (for testing) - ALL // Render all scenarios -}; - -struct debug_options { - std::string template_path; - bool with_tools = true; - bool generation_prompt = true; - bool enable_reasoning = true; - bool debug_jinja = false; - bool force_tool_call = false; - bool parallel_tool_calls = true; - output_mode mode = output_mode::BOTH; - input_message_type input_message = input_message_type::NONE; -}; - -static std::string read_file(const std::string & path) { - std::ifstream fin(path, std::ios::binary); - if (!fin.is_open()) { - throw std::runtime_error("Could not open file: " + path); - } - std::ostringstream buf; - buf << fin.rdbuf(); - return buf.str(); -} - -static std::string read_gguf_chat_template(const std::string & path) { - struct gguf_init_params params = { /*no_alloc =*/true, // We only need metadata, not tensor data - /*ctx=*/nullptr }; - - struct gguf_context * ctx = gguf_init_from_file(path.c_str(), params); - if (ctx == nullptr) { - throw std::runtime_error("Could not open GGUF file: " + path); - } - - const char * key = "tokenizer.chat_template"; - int64_t key_id = gguf_find_key(ctx, key); - - if (key_id == -1) { - gguf_free(ctx); - throw std::runtime_error("GGUF file does not contain chat template key: " + std::string(key)); - } - - const char * template_str = gguf_get_val_str(ctx, key_id); - if (template_str == nullptr) { - gguf_free(ctx); - throw std::runtime_error("GGUF file contains chat template key but value is null"); - } - - std::string result = template_str; - gguf_free(ctx); - return result; -} - -static void print_usage(const char * program_name) { - LOG_ERR("Usage: %s [options]\n", program_name); - LOG_ERR("\nOptions:\n"); - LOG_ERR(" --no-tools Disable tool definitions\n"); - LOG_ERR(" --force-tool-call Set tool calls to forced\n"); - LOG_ERR(" --parallel-tool-calls=0|1 Set parallel_tool_calls (default: 1)\n"); - LOG_ERR(" --generation-prompt=0|1 Set add_generation_prompt (default: 1)\n"); - LOG_ERR(" --enable-reasoning=0|1 Enable reasoning parsing (default: 1)\n"); - LOG_ERR(" --output=MODE Output mode: analysis, template, both (default: both)\n"); - LOG_ERR(" --debug-jinja Enable Jinja fine-grained debug\n"); - LOG_ERR(" --input-message=TYPE Message type to render:\n"); - LOG_ERR(" content_only, reasoning_content, tool_call_only,\n"); - LOG_ERR(" content_tool_call, reasoning_tool_call,\n"); - LOG_ERR(" content_fake_tool_call, all\n"); - LOG_ERR("\nExamples:\n"); - LOG_ERR(" %s template.jinja --input-message=all --generation-prompt=1\n", program_name); - LOG_ERR(" %s template.jinja --output=template --input-message=tool_call_only\n", program_name); -} - -static bool parse_bool_option(const std::string & value) { - return value == "1" || value == "true" || value == "yes"; -} - -static bool parse_options(int argc, char ** argv, debug_options & opts) { - if (argc < 2) { - print_usage(argv[0]); - return false; - } - - opts.template_path = argv[1]; - - for (int i = 2; i < argc; ++i) { - std::string arg = argv[i]; - - if (arg == "--force-tool-call") { - opts.force_tool_call = true; - } else if (arg == "--debug-jinja") { - opts.debug_jinja = true; - } else if (arg == "--no-tools") { - opts.with_tools = false; - } else if (arg.rfind("--parallel-tool-calls=", 0) == 0) { - opts.parallel_tool_calls = parse_bool_option(arg.substr(22)); - } else if (arg.rfind("--generation-prompt=", 0) == 0) { - opts.generation_prompt = parse_bool_option(arg.substr(20)); - } else if (arg.rfind("--enable-reasoning=", 0) == 0) { - opts.enable_reasoning = parse_bool_option(arg.substr(19)); - } else if (arg.rfind("--output=", 0) == 0) { - std::string mode = arg.substr(9); - if (mode == "analysis") { - opts.mode = output_mode::ANALYSIS; - } else if (mode == "template") { - opts.mode = output_mode::TEMPLATE; - } else if (mode == "both") { - opts.mode = output_mode::BOTH; - } else { - LOG_ERR("Unknown output mode: %s\n", mode.c_str()); - return false; - } - } else if (arg.rfind("--input-message=", 0) == 0) { - std::string type = arg.substr(16); - if (type == "content_only") { - opts.input_message = input_message_type::CONTENT_ONLY; - } else if (type == "reasoning_content") { - opts.input_message = input_message_type::REASONING_CONTENT; - } else if (type == "tool_call_only") { - opts.input_message = input_message_type::TOOL_CALL_ONLY; - } else if (type == "content_tool_call") { - opts.input_message = input_message_type::CONTENT_TOOL_CALL; - } else if (type == "reasoning_tool_call") { - opts.input_message = input_message_type::REASONING_TOOL_CALL; - } else if (type == "content_fake_tool_call") { - opts.input_message = input_message_type::CONTENT_FAKE_TOOL_CALL; - } else if (type == "all") { - opts.input_message = input_message_type::ALL; - } else { - LOG_ERR("Unknown input message type: %s\n", type.c_str()); - return false; - } - } else { - LOG_ERR("Unknown option: %s\n", arg.c_str()); - print_usage(argv[0]); - return false; - } - } - - return true; -} - -static json build_user_message() { - return json{ - { "role", "user" }, - { "content", "Hello, please help me with a task." } - }; -} - -static json build_content_only_message() { - return json{ - { "role", "assistant" }, - { "content", "Hello! I'm here to help you with your task." } - }; -} - -static json build_reasoning_content_message() { - return json{ - { "role", "assistant" }, - { "content", "Hello! I'm here to help you with your task." }, - { "reasoning_content", "The user is greeting me and asking for help. I should respond politely." } - }; -} - -static json build_tool_call_only_message() { - return json{ - { "role", "assistant" }, - { "content", nullptr }, - { "tool_calls", - json::array({ json{ - { "type", "function" }, - { "function", json{ { "name", "test_function_name" }, - { "arguments", json::object({ { "param1", "value1" }, { "param2", "value2" } }) } } }, - { "id", "123456789" } } }) } - }; -} - -static json build_content_tool_call_message() { - return json{ - { "role", "assistant" }, - { "content", "I'll help you by calling a function." }, - { "tool_calls", - json::array({ json{ - { "type", "function" }, - { "function", - json{ { "name", "test_function_name" }, - { "arguments", json::object({ { "param1", "value1" }, { "param2", "value2" } }) } } } } }) } - }; -} - -static json build_reasoning_tool_call_message() { - return json{ - { "role", "assistant" }, - { "content", nullptr }, - { "reasoning_content", "I need to call a function to help with this task." }, - { "tool_calls", - json::array({ json{ - { "type", "function" }, - { "function", - json{ { "name", "test_function_name" }, - { "arguments", json::object({ { "param1", "value1" }, { "param2", "value2" } }) } } } } }) } - }; -} - -static json build_content_fake_tool_call_message() { - // This message has content but NO tool_calls field - // It's used to test if a template renders tool definitions but not tool calls - return json{ - { "role", "assistant" }, - { "content", "I'll help you by calling a function." } - }; -} - -static json build_tools_definition() { - json parameters_schema = json::object(); - parameters_schema["type"] = "object"; - parameters_schema["properties"] = json::object(); - parameters_schema["properties"]["param1"] = json::object({ - { "type", "string" }, - { "description", "First parameter" } - }); - parameters_schema["properties"]["param2"] = json::object({ - { "type", "string" }, - { "description", "Second parameter" } - }); - parameters_schema["required"] = json::array({ "param1" }); - - return json::array({ - json{ { "type", "function" }, - { "function", json{ { "name", "test_function_name" }, - { "description", "A test function for debugging" }, - { "parameters", parameters_schema } } } } - }); -} - -static void render_scenario(const common_chat_template & tmpl, - const std::string & scenario_name, - const json & messages, - const json & tools, - bool add_generation_prompt, - bool enable_thinking) { - LOG_ERR("\n=== Scenario: %s ===\n", scenario_name.c_str()); - LOG_ERR("add_generation_prompt: %s, enable_thinking: %s\n", add_generation_prompt ? "true" : "false", - enable_thinking ? "true" : "false"); - - // When add_generation_prompt is true, add a trailing user message to trigger the prompt - json final_messages = messages; - if (add_generation_prompt && !messages.empty() && messages.back().value("role", "") == "assistant") { - final_messages.push_back(json{ - { "role", "user" }, - { "content", "Now please continue with another response." } - }); - } - - LOG_ERR("Messages:\n%s\n", final_messages.dump(2).c_str()); - - try { - autoparser::generation_params inputs; - inputs.messages = final_messages; - inputs.add_generation_prompt = add_generation_prompt; - inputs.extra_context["enable_thinking"] = enable_thinking; - - if (!tools.is_null() && tools.is_array() && !tools.empty()) { - inputs.tools = tools; - } - - std::string output = common_chat_template_direct_apply(tmpl, inputs); - - LOG_ERR("\n--- Rendered Output ---\n"); - LOG_ERR("%s\n", output.c_str()); - LOG_ERR("--- End Output (length: %zu) ---\n", output.length()); - } catch (const std::exception & e) { - LOG_ERR("Rendering failed: %s\n", e.what()); - } -} - -static void render_all_scenarios(const common_chat_template & tmpl, - const json & tools, - bool add_generation_prompt, - bool enable_thinking, - input_message_type message_type) { - json user_msg = build_user_message(); - - auto render_if = [&](input_message_type type, const std::string & name, const json & assistant_msg) { - if (message_type == input_message_type::ALL || message_type == type) { - json messages = json::array({ user_msg, assistant_msg }); - render_scenario(tmpl, name, messages, tools, add_generation_prompt, enable_thinking); - } - }; - - render_if(input_message_type::CONTENT_ONLY, "content_only", build_content_only_message()); - render_if(input_message_type::REASONING_CONTENT, "reasoning_content", build_reasoning_content_message()); - render_if(input_message_type::TOOL_CALL_ONLY, "tool_call_only", build_tool_call_only_message()); - render_if(input_message_type::CONTENT_TOOL_CALL, "content_tool_call", build_content_tool_call_message()); - render_if(input_message_type::REASONING_TOOL_CALL, "reasoning_tool_call", build_reasoning_tool_call_message()); - render_if(input_message_type::CONTENT_FAKE_TOOL_CALL, "content_fake_tool_call", - build_content_fake_tool_call_message()); - - // Also render with add_generation_prompt=true to show the prompt ending - if (message_type == input_message_type::ALL) { - LOG_ERR("\n\n=== Generation Prompt Scenarios (add_generation_prompt=true) ===\n"); - - json prompt_messages = json::array({ user_msg }); - render_scenario(tmpl, "generation_prompt_only", prompt_messages, tools, true, enable_thinking); - - // With enable_thinking toggled - render_scenario(tmpl, "generation_prompt_thinking_disabled", prompt_messages, tools, true, false); - } -} - -static autoparser::generation_params prepare_params(const debug_options & opts, const json & tools) { - autoparser::generation_params params; - params.messages = json::array({ build_user_message() }); - params.reasoning_format = opts.enable_reasoning ? COMMON_REASONING_FORMAT_DEEPSEEK : COMMON_REASONING_FORMAT_NONE; - params.enable_thinking = opts.enable_reasoning; - params.add_generation_prompt = opts.generation_prompt; - - if (opts.with_tools) { - params.tools = tools; - params.tool_choice = opts.force_tool_call ? COMMON_CHAT_TOOL_CHOICE_REQUIRED : COMMON_CHAT_TOOL_CHOICE_AUTO; - } else { - params.tools = json(); - params.tool_choice = COMMON_CHAT_TOOL_CHOICE_NONE; - } - params.parallel_tool_calls = opts.parallel_tool_calls; - return params; -} - -int main(int argc, char ** argv) { - // Set log level to most verbose to capture all debug output - common_log_set_verbosity_thold(99); - - debug_options opts; - if (!parse_options(argc, argv, opts)) { - return 1; - } - - if (opts.debug_jinja || std::getenv("LLAMA_DEBUG_JINJA") != nullptr) { - jinja::enable_debug(true); - } - - std::string template_source; - try { - // Check if the file is a GGUF file - if (opts.template_path.size() >= 5 && - opts.template_path.compare(opts.template_path.size() - 5, 5, ".gguf") == 0) { - template_source = read_gguf_chat_template(opts.template_path); - } else { - template_source = read_file(opts.template_path); - } - } catch (const std::exception & e) { - LOG_ERR("Error reading template: %s\n", e.what()); - return 1; - } - - LOG_ERR("Analyzing template: %s\n", opts.template_path.c_str()); - LOG_ERR("Options: with_tools=%s, generation_prompt=%s, enable_reasoning=%s\n", opts.with_tools ? "true" : "false", - opts.generation_prompt ? "true" : "false", opts.enable_reasoning ? "true" : "false"); - - try { - common_chat_template chat_template(template_source, "", ""); - - json tools = opts.with_tools ? build_tools_definition() : json(); - - autoparser::generation_params params = prepare_params(opts, tools); - common_chat_params parser_data; - if (std::optional spec_tmpl = - common_chat_try_specialized_template(chat_template, template_source, params)) { - LOG_ERR("\n"); - LOG_ERR("This template uses a specialized parser, analysis results will not be available.\n"); - parser_data = *spec_tmpl; - } else { - // Render template scenarios if requested - if (opts.input_message != input_message_type::NONE && - (opts.mode == output_mode::TEMPLATE || opts.mode == output_mode::BOTH)) { - LOG_ERR("\n"); - LOG_ERR("================================================================================\n"); - LOG_ERR(" TEMPLATE RENDERING OUTPUT\n"); - LOG_ERR("================================================================================\n"); - - render_all_scenarios(chat_template, tools, opts.generation_prompt, opts.enable_reasoning, - opts.input_message); - } - - // Output analysis if requested - if (opts.mode == output_mode::ANALYSIS || opts.mode == output_mode::BOTH) { - LOG_ERR("\n"); - LOG_ERR("================================================================================\n"); - LOG_ERR(" TEMPLATE ANALYSIS\n"); - LOG_ERR("================================================================================\n"); - - autoparser::autoparser analysis; - analysis.analyze_template(chat_template); - - // Generate Parser - parser_data = autoparser::peg_generator::generate_parser(chat_template, params, analysis); - } - } - - if (!std::empty(parser_data.parser)) { - LOG_ERR("\n=== Generated Parser ===\n"); - common_peg_arena arena; - arena.load(parser_data.parser); - LOG_ERR("%s\n", arena.dump(arena.root()).c_str()); - - LOG_ERR("\n=== Generated Grammar ===\n"); - LOG_ERR("%s\n", parser_data.grammar.c_str()); - - LOG_ERR("\n=== Generated Lazy Grammar ===\n"); - LOG_ERR("%d\n", parser_data.grammar_lazy); - - LOG_ERR("\n=== Generated Grammar Triggers ===\n"); - for (const common_grammar_trigger & cgt : parser_data.grammar_triggers) { - LOG_ERR("Token: %d | Type: %d | Value: %s\n", cgt.token, cgt.type, cgt.value.c_str()); - } - - LOG_ERR("\n=== Preserved Tokens ===\n"); - for (const std::string & token : parser_data.preserved_tokens) { - LOG_ERR(" '%s'\n", token.c_str()); - } - - if (!parser_data.grammar.empty()) { - LOG_ERR("\n=== Verifying created grammar ===\n"); - auto * grammar = llama_grammar_init_impl(nullptr, parser_data.grammar.c_str(), "root", - parser_data.grammar_lazy, nullptr, 0, nullptr, 0); - if (grammar != nullptr) { - LOG_ERR("\n=== Grammar successfully created ===\n"); - } - } - } - } catch (const std::exception & e) { - LOG_ERR("Analysis failed: %s\n", e.what()); - return 1; - } - - return 0; -} From 8d9af256337d1a501250f9bbf4c0859a654bddd6 Mon Sep 17 00:00:00 2001 From: Georgi Gerganov Date: Sun, 23 Aug 2026 19:59:42 +0300 Subject: [PATCH 16/28] test : fix multi-GPU server tests (#27614) * tests : fix tests for multi-gpu environment * cont : not needed --- tools/server/tests/unit/test_slot_save.py | 21 ++++++++++++++++++++- tools/server/tests/unit/test_vision_api.py | 2 +- tools/server/tests/utils.py | 2 +- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/tools/server/tests/unit/test_slot_save.py b/tools/server/tests/unit/test_slot_save.py index 05acb1be1..5af61d70d 100644 --- a/tools/server/tests/unit/test_slot_save.py +++ b/tools/server/tests/unit/test_slot_save.py @@ -319,7 +319,6 @@ def test_slot_save_restore_with_two_images(mmproj_server): "prompt": prompt, }) assert res.status_code == 200 - content = res.body["content"] prompt_n_full = res.body["timings"]["prompt_n"] assert prompt_n_full > 64 @@ -345,6 +344,26 @@ def test_slot_save_restore_with_two_images(mmproj_server): assert res.status_code == 200 assert res.body["timings"]["cache_n"] == prompt_n_full - 1 assert res.body["timings"]["prompt_n"] == 1 + content = res.body["content"] + + res = server.make_request("POST", "/slots/1?action=restore", data={ + "filename": "mm_slot_two_images.bin", + }) + assert res.status_code == 200 + assert res.body["n_restored"] == n_saved + + res = server.make_request("POST", "/completions", data={ + "temperature": 0.0, + "top_k": 1, + "id_slot": 0, + "cache_prompt": True, + "prompt": prompt, + }) + assert res.status_code == 200 + assert res.body["timings"]["cache_n"] == prompt_n_full - 1 + assert res.body["timings"]["prompt_n"] == 1 + content = res.body["content"] + assert res.body["content"] == content diff --git a/tools/server/tests/unit/test_vision_api.py b/tools/server/tests/unit/test_vision_api.py index d74cc3a43..8b01c5372 100644 --- a/tools/server/tests/unit/test_vision_api.py +++ b/tools/server/tests/unit/test_vision_api.py @@ -121,7 +121,7 @@ def test_vision_chat_completion_token_count(): "prompt, image_data, success, re_content", [ # test model is trained on CIFAR-10, but it's quite dumb due to small size - ("What is this: <__media__>\n", "IMG_BASE64_0", True, "(cat)+"), + ("What is this: <__media__>\n", "IMG_BASE64_0", True, "(cat)+|(automobile)+"), ("What is this: <__media__>\n", "IMG_BASE64_1", True, "(frog)+"), ("What is this: <__media__>\n", "malformed", False, None), # non-image data ("What is this:\n", "", False, None), # empty string diff --git a/tools/server/tests/utils.py b/tools/server/tests/utils.py index 9171dbc02..a0d2dfa3c 100644 --- a/tools/server/tests/utils.py +++ b/tools/server/tests/utils.py @@ -623,7 +623,7 @@ class ServerPreset: server.model_hf_repo = "ggml-org/tinygemma3-GGUF:Q8_0" server.model_alias = "tinygemma3" server.n_ctx = 1024 - server.n_batch = 32 + server.n_batch = 512 server.n_slots = 2 server.n_predict = 4 server.seed = 42 From d05f89562d50e16e8c025081aa03bf3e449efd82 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Sun, 23 Aug 2026 19:37:19 +0200 Subject: [PATCH 17/28] fix: Change chat tabs nav shortcuts (#27609) --- tools/ui/src/lib/enums/keyboard.enums.ts | 2 ++ tools/ui/src/lib/hooks/use-keyboard-shortcuts.svelte.ts | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/ui/src/lib/enums/keyboard.enums.ts b/tools/ui/src/lib/enums/keyboard.enums.ts index b5749211d..3fde816f6 100644 --- a/tools/ui/src/lib/enums/keyboard.enums.ts +++ b/tools/ui/src/lib/enums/keyboard.enums.ts @@ -7,6 +7,8 @@ export enum KeyboardKey { ARROW_RIGHT = 'ArrowRight', ARROW_UP = 'ArrowUp', B_LOWER = 'b', + BRACKET_LEFT = 'BracketLeft', + BRACKET_RIGHT = 'BracketRight', D_LOWER = 'd', D_UPPER = 'D', E_UPPER = 'E', diff --git a/tools/ui/src/lib/hooks/use-keyboard-shortcuts.svelte.ts b/tools/ui/src/lib/hooks/use-keyboard-shortcuts.svelte.ts index 7d8249b72..eef1bc332 100644 --- a/tools/ui/src/lib/hooks/use-keyboard-shortcuts.svelte.ts +++ b/tools/ui/src/lib/hooks/use-keyboard-shortcuts.svelte.ts @@ -86,12 +86,12 @@ export function useKeyboardShortcuts(callbacks: KeyboardShortcutsCallbacks) { callbacks.navigateToNextConversation?.(); } - if (isCmdOrCtrl && event.shiftKey && event.key === KeyboardKey.ARROW_LEFT) { + if (isCmdOrCtrl && event.altKey && event.shiftKey && event.code === KeyboardKey.BRACKET_LEFT) { event.preventDefault(); callbacks.navigateToPrevTab?.(); } - if (isCmdOrCtrl && event.shiftKey && event.key === KeyboardKey.ARROW_RIGHT) { + if (isCmdOrCtrl && event.altKey && event.shiftKey && event.code === KeyboardKey.BRACKET_RIGHT) { event.preventDefault(); callbacks.navigateToNextTab?.(); } From ccc8fd2baa64ab26c7210b02765d3604069ec7ee Mon Sep 17 00:00:00 2001 From: Georgi Gerganov Date: Sun, 23 Aug 2026 20:55:56 +0300 Subject: [PATCH 18/28] readme : update links (#27617) * readme : update links * readme : update maintainer PRs list Add the new members of the `ggml-org` `maintainers` team to the author filter of the maintainer PRs link (nikwen, marty1885, Titaniumtown), keeping the canonical team ordering. The list now matches the team exactly (35 members). Assisted-by: pi:llama.cpp/Qwen3.8-27B --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1f2076038..0b5598c6e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ [![Docker](https://img.shields.io/github/actions/workflow/status/ggml-org/llama.cpp/docker.yml?label=Docker)](https://github.com/ggml-org/llama.cpp/actions/workflows/docker.yml) [![Winget](https://img.shields.io/github/actions/workflow/status/ggml-org/llama.cpp/winget.yml?label=Winget)](https://github.com/ggml-org/llama.cpp/actions/workflows/winget.yml) -[manifesto](https://github.com/ggml-org/llama.cpp/discussions/205) / [ggml](https://github.com/ggml-org/ggml) / [ops](https://github.com/ggml-org/llama.cpp/blob/master/docs/ops.md) / [maintainer PRs](https://github.com/ggml-org/llama.cpp/issues?q=is%3Apr%20is%3Aopen%20draft%3AFalse%20(author%3Argerganov%20OR%20author%3AKitaitiMakoto%20OR%20author%3Adanbev%20OR%20author%3Aaldehir%20OR%20author%3Amax-krasnyansky%20OR%20author%3ACISC%20OR%20author%3Aggerganov%20OR%20author%3Aam17an%20OR%20author%3Abartowski1182%20OR%20author%3Ahipudding%20OR%20author%3AServeurpersoCom%20OR%20author%3Apwilkin%20OR%20author%3Areeselevine%20OR%20author%3Angxson%20OR%20author%3Ajeffbolznv%20OR%20author%3A0cc4m%20OR%20author%3Aangt%20OR%20author%3AIMbackK%20OR%20author%3Aarthw%20OR%20author%3AJohannesGaessler%20OR%20author%3AORippler%20OR%20author%3Aruixiang63%20OR%20author%3Axctan%20OR%20author%3Aallozaur%20OR%20author%3Ayomaytk%20OR%20author%3Aaendk%20OR%20author%3Agaugarg-nv%20OR%20author%3Ataronaeo%20OR%20author%3Aforforever73%20OR%20author%3Alhez%20OR%20author%3Anetrunnereve%20OR%20author%3Afairydreaming)%20sort%3Aupdated-desc) / [compile times](https://github.com/ggml-org/llama.cpp-dev/blob/master/README-compile-times.md) / [lib llama API](https://github.com/ggml-org/llama.cpp/issues/9289) / [llama-server REST API](https://github.com/ggml-org/llama.cpp/issues/9291) +[ggml](https://github.com/ggml-org/ggml) / [ops](https://github.com/ggml-org/llama.cpp/blob/master/docs/ops.md) / [maintainer PRs](https://github.com/ggml-org/llama.cpp/issues?q=is%3Apr%20is%3Aopen%20draft%3AFalse%20(author%3Argerganov%20OR%20author%3AKitaitiMakoto%20OR%20author%3Adanbev%20OR%20author%3Aaldehir%20OR%20author%3Amax-krasnyansky%20OR%20author%3ACISC%20OR%20author%3Aggerganov%20OR%20author%3Aam17an%20OR%20author%3Abartowski1182%20OR%20author%3Anikwen%20OR%20author%3Ahipudding%20OR%20author%3AServeurpersoCom%20OR%20author%3Apwilkin%20OR%20author%3Areeselevine%20OR%20author%3Angxson%20OR%20author%3Ajeffbolznv%20OR%20author%3Amarty1885%20OR%20author%3A0cc4m%20OR%20author%3ATitaniumtown%20OR%20author%3Aangt%20OR%20author%3AIMbackK%20OR%20author%3Aarthw%20OR%20author%3AJohannesGaessler%20OR%20author%3AORippler%20OR%20author%3Aruixiang63%20OR%20author%3Axctan%20OR%20author%3Aallozaur%20OR%20author%3Ayomaytk%20OR%20author%3Aaendk%20OR%20author%3Agaugarg-nv%20OR%20author%3Ataronaeo%20OR%20author%3Aforforever73%20OR%20author%3Alhez%20OR%20author%3Anetrunnereve%20OR%20author%3Afairydreaming)%20sort%3Aupdated-desc) / [dev stats](https://github.com/ggml-org/llama.cpp-dev) / [lib llama API](https://github.com/ggml-org/llama.cpp/issues/9289) / [llama-server REST API](https://github.com/ggml-org/llama.cpp/issues/9291)
From c060ca974c773c7c3d17fd1b66dc9d312bc292c0 Mon Sep 17 00:00:00 2001 From: jacekpoplawski <67507230+jacekpoplawski@users.noreply.github.com> Date: Sun, 23 Aug 2026 20:20:44 +0200 Subject: [PATCH 19/28] model : support MTP in GLM-4.5-Air (#26534) --- conversion/glm.py | 47 ++++++++- gguf-py/gguf/constants.py | 2 +- src/models/glm4-moe.cpp | 202 +++++++++++++++++++++++++++++++++++--- src/models/models.h | 4 + 4 files changed, 233 insertions(+), 22 deletions(-) diff --git a/conversion/glm.py b/conversion/glm.py index abd7f279f..23fdbca88 100644 --- a/conversion/glm.py +++ b/conversion/glm.py @@ -112,12 +112,36 @@ class GlmOCRModel(Glm4Model): @ModelBase.example("zai-org/GLM-4.5-Air") class Glm4MoeModel(TextModel): model_arch = gguf.MODEL_ARCH.GLM4_MOE + supports_mtp_export = True + _n_main_layers: int | None = None def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # GLM4_MOE has num_hidden_layers + 1 actual layers (including NextN layer) - self.block_count = self.hparams["num_hidden_layers"] + self.hparams.get("num_nextn_predict_layers", 0) - self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count) + if not self.no_mtp: + self.block_count += self.hparams.get("num_nextn_predict_layers", 0) + self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count) + + def index_tensors(self, remote_hf_model_id: str | None = None): + type(self)._n_main_layers = self.hparams["num_hidden_layers"] + return super().index_tensors(remote_hf_model_id=remote_hf_model_id) + + @classmethod + def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None: + if (titem := super().filter_tensors(item)) is None: + return None + name, gen = titem + + assert cls._n_main_layers is not None + is_mtp = (m := re.match(r"model\.layers\.(\d+)\.", name)) is not None and int(m.group(1)) >= cls._n_main_layers + + if is_mtp and cls.no_mtp: + return None + if cls.mtp_only and not is_mtp and name not in ( + "model.embed_tokens.weight", "model.norm.weight", "lm_head.weight", + ): + return None + + return name, gen def set_vocab(self): return self._set_vocab_glm() @@ -153,10 +177,22 @@ class Glm4MoeModel(TextModel): if (norm_topk_prob := self.hparams.get("norm_topk_prob")) is not None: self.gguf_writer.add_expert_weights_norm(norm_topk_prob) - # NextN/MTP prediction layers - if (num_nextn_predict_layers := self.hparams.get("num_nextn_predict_layers")) is not None: + if not self.no_mtp and (num_nextn_predict_layers := self.hparams.get("num_nextn_predict_layers")) is not None: self.gguf_writer.add_nextn_predict_layers(num_nextn_predict_layers) + def prepare_metadata(self, vocab_only: bool): + from_dir = self.fname_out.is_dir() + super().prepare_metadata(vocab_only=vocab_only) + + if not self.mtp_only or not from_dir: + return + + output_type: str = self.ftype.name.partition("_")[2] + fname_default: str = gguf.naming_convention( + self.metadata.name, self.metadata.basename, self.metadata.finetune, + self.metadata.version, size_label=None, output_type=output_type, model_type=None) + self.fname_out = self.fname_out.parent / f"mtp-{fname_default}.gguf" + _experts: list[dict[str, Tensor]] | None = None # note: unlike GLM4V non-MoE, we don't need to permute Q/K here since GLM4V_MOE uses Neox ordering already @@ -348,6 +384,7 @@ class GlmMoeDsaModel(DeepseekV2Model): @ModelBase.example("upstage/Solar-Open-100B") class SolarOpenModel(Glm4MoeModel): model_arch = gguf.MODEL_ARCH.GLM4_MOE + supports_mtp_export = False def set_vocab(self): from transformers import AutoTokenizer diff --git a/gguf-py/gguf/constants.py b/gguf-py/gguf/constants.py index 8f6f55519..f236a5d2c 100644 --- a/gguf-py/gguf/constants.py +++ b/gguf-py/gguf/constants.py @@ -3822,7 +3822,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = { MODEL_TENSOR.FFN_DOWN_SHEXP, MODEL_TENSOR.FFN_UP_SHEXP, MODEL_TENSOR.FFN_EXP_PROBS_B, - # NextN/MTP tensors - preserved but unused + # NextN/MTP tensors MODEL_TENSOR.NEXTN_EH_PROJ, MODEL_TENSOR.NEXTN_EMBED_TOKENS, MODEL_TENSOR.NEXTN_ENORM, diff --git a/src/models/glm4-moe.cpp b/src/models/glm4-moe.cpp index 8cde66978..83ea7f8ac 100644 --- a/src/models/glm4-moe.cpp +++ b/src/models/glm4-moe.cpp @@ -29,10 +29,19 @@ void llama_model_glm4_moe::load_arch_hparams(llama_model_loader & ml) { } } -void llama_model_glm4_moe::load_arch_tensors(llama_model_loader &) { +void llama_model_glm4_moe::load_arch_tensors(llama_model_loader & ml) { LLAMA_LOAD_LOCALS; const int64_t n_expert_shared = hparams.n_expert_shared; + const bool mtp_only = (hparams.n_layer_nextn > 0) && (ml.get_weight("blk.0.attn_norm.weight") == nullptr); + const std::string mtp_probe = "blk." + std::to_string(n_layer) + ".nextn.eh_proj.weight"; + const bool trunk_only = (hparams.n_layer_nextn > 0) && (ml.get_weight(mtp_probe.c_str()) == nullptr); + const int trunk_flags = mtp_only ? TENSOR_NOT_REQUIRED : 0; + int mtp_flags = trunk_only ? TENSOR_NOT_REQUIRED : 0; + + if (!ml.load_mtp) { + mtp_flags |= TENSOR_SKIP; + } GGML_ASSERT(hparams.n_expert > 0 && "n_expert must be > 0 for GLM4_MOE MoE layers"); GGML_ASSERT(hparams.n_expert_used > 0 && "n_expert_used must be > 0 for GLM4_MOE MoE layers"); @@ -47,16 +56,9 @@ void llama_model_glm4_moe::load_arch_tensors(llama_model_loader &) { output = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), { n_embd, n_vocab }, TENSOR_DUPLICATED); } - // Load ALL tensors including NextN layer to satisfy total tensor count - // but only PROCESS up to last layer (skipping final NextN layer) in forward pass for (int i = 0; i < n_layer_all; ++i) { - int flags = 0; - if (i >= n_layer) { - // skip all tensors in the NextN layers - flags |= TENSOR_SKIP; - } - auto & layer = layers[i]; + const int flags = i < n_layer ? trunk_flags : mtp_flags; layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i), { n_embd }, flags); @@ -110,24 +112,186 @@ void llama_model_glm4_moe::load_arch_tensors(llama_model_loader &) { layer.ffn_up = create_tensor(tn(LLM_TENSOR_FFN_UP, "weight", i), { n_embd, n_ff }, flags); } - // NextN/MTP tensors (preserved but unused) - conditionally load for last nextn_predict_layers + // NextN/MTP tensors if (i >= n_layer) { layer.nextn.eh_proj = create_tensor(tn(LLM_TENSOR_NEXTN_EH_PROJ, "weight", i), { 2 * n_embd, n_embd }, flags); layer.nextn.enorm = create_tensor(tn(LLM_TENSOR_NEXTN_ENORM, "weight", i), { n_embd }, flags); layer.nextn.hnorm = create_tensor(tn(LLM_TENSOR_NEXTN_HNORM, "weight", i), { n_embd }, flags); // Optional tensors - layer.nextn.embed_tokens = create_tensor(tn(LLM_TENSOR_NEXTN_EMBED_TOKENS, "weight", i), { n_embd, n_vocab }, flags | TENSOR_NOT_REQUIRED); - layer.nextn.shared_head_head = create_tensor(tn(LLM_TENSOR_NEXTN_SHARED_HEAD_HEAD, "weight", i), { n_embd, n_vocab }, flags | TENSOR_NOT_REQUIRED); - layer.nextn.shared_head_norm = create_tensor(tn(LLM_TENSOR_NEXTN_SHARED_HEAD_NORM, "weight", i), { n_embd }, flags | TENSOR_NOT_REQUIRED); + layer.nextn.embed_tokens = create_tensor(tn(LLM_TENSOR_NEXTN_EMBED_TOKENS, "weight", i), { n_embd, n_vocab }, TENSOR_NOT_REQUIRED | flags); + layer.nextn.shared_head_head = create_tensor(tn(LLM_TENSOR_NEXTN_SHARED_HEAD_HEAD, "weight", i), { n_embd, n_vocab }, TENSOR_NOT_REQUIRED | flags); + layer.nextn.shared_head_norm = create_tensor(tn(LLM_TENSOR_NEXTN_SHARED_HEAD_NORM, "weight", i), { n_embd }, TENSOR_NOT_REQUIRED | flags); } } } std::unique_ptr llama_model_glm4_moe::build_arch_graph(const llm_graph_params & params) const { + if (params.gtype == LLM_GRAPH_TYPE_DECODER_MTP) { + return std::make_unique(*this, params); + } return std::make_unique(*this, params); } +llama_model_glm4_moe::graph_mtp::graph_mtp(const llama_model & model, const llm_graph_params & params) + : llm_graph_context(params) { + GGML_ASSERT(hparams.n_layer_nextn > 0 && "GLM4_MOE MTP requires n_layer_nextn > 0"); + GGML_ASSERT(hparams.n_layer_nextn == 1 && "GLM4_MOE MTP currently only supports a single MTP block"); + + const int64_t n_embd_head = hparams.n_embd_head_v(); + GGML_ASSERT(n_embd_head == hparams.n_embd_head_k()); + + const int il = hparams.n_layer() + cparams.nextn_layer_offset; + 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)"); + + const auto & layer = model.layers[il]; + + GGML_ASSERT(layer.nextn.eh_proj && "MTP block missing nextn.eh_proj"); + GGML_ASSERT(layer.nextn.enorm && "MTP block missing nextn.enorm"); + GGML_ASSERT(layer.nextn.hnorm && "MTP block missing nextn.hnorm"); + GGML_ASSERT(layer.ffn_gate_inp && "MTP block missing ffn_gate_inp"); + + auto inp = std::make_unique(hparams.n_embd); + + inp->tokens = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_tokens); + ggml_set_input(inp->tokens); + + inp->embd = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, hparams.n_embd_inp(), n_tokens); + ggml_set_input(inp->embd); + + ggml_tensor * tok_embd; + 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); + } else { + tok_embd = inp->embd; + } + cb(tok_embd, "mtp_tok_embd", il); + + inp->h = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, hparams.n_embd, n_tokens); + ggml_set_input(inp->h); + ggml_set_name(inp->h, "mtp_h_input"); + + ggml_tensor * h_embd = inp->h; + + res->add_input(std::move(inp)); + + ggml_tensor * inp_pos = build_inp_pos(); + ggml_tensor * inp_out_ids = build_inp_out_ids(); + + auto * inp_attn = build_attn_inp_kv(); + + ggml_tensor * h_norm = build_norm(h_embd, layer.nextn.hnorm, nullptr, LLM_NORM_RMS, il); + cb(h_norm, "mtp_hnorm", il); + + ggml_tensor * e_norm = build_norm(tok_embd, layer.nextn.enorm, nullptr, LLM_NORM_RMS, il); + cb(e_norm, "mtp_enorm", il); + + ggml_tensor * concat = ggml_concat(ctx0, e_norm, h_norm, 0); + cb(concat, "mtp_concat", il); + + ggml_tensor * cur = build_lora_mm(layer.nextn.eh_proj, concat, layer.nextn.eh_proj_s); + cb(cur, "mtp_eh_proj", il); + + ggml_tensor * inpSA = cur; + + cur = build_norm(cur, layer.attn_norm, nullptr, LLM_NORM_RMS, il); + cb(cur, "mtp_attn_norm", il); + + auto [Qcur, Kcur, Vcur] = build_qkv(layer, cur, + n_embd_head, n_head, n_head_kv, il); + + if (layer.attn_q_norm) { + Qcur = build_norm(Qcur, layer.attn_q_norm, nullptr, LLM_NORM_RMS, il); + cb(Qcur, "mtp_Qcur_normed", il); + } + if (layer.attn_k_norm) { + Kcur = build_norm(Kcur, layer.attn_k_norm, nullptr, LLM_NORM_RMS, il); + cb(Kcur, "mtp_Kcur_normed", il); + } + + Qcur = ggml_rope_ext(ctx0, Qcur, inp_pos, nullptr, n_rot, + rope_type, n_ctx_orig, freq_base, freq_scale, + ext_factor, attn_factor, beta_fast, beta_slow); + + Kcur = ggml_rope_ext(ctx0, Kcur, inp_pos, nullptr, n_rot, + rope_type, n_ctx_orig, freq_base, freq_scale, + ext_factor, attn_factor, beta_fast, beta_slow); + + cb(Qcur, "mtp_Qcur", il); + cb(Kcur, "mtp_Kcur", il); + cb(Vcur, "mtp_Vcur", il); + + cur = build_attn(inp_attn, + layer.wo, nullptr, layer.wo_s, + Qcur, Kcur, Vcur, nullptr, nullptr, nullptr, + 1.0f / sqrtf(float(n_embd_head)), il); + cb(cur, "mtp_attn_out", il); + + ggml_tensor * ffn_inp = ggml_add(ctx0, cur, inpSA); + cb(ffn_inp, "mtp_ffn_inp", il); + + cur = build_norm(ffn_inp, layer.attn_post_norm, nullptr, LLM_NORM_RMS, il); + cb(cur, "mtp_post_attn_norm", il); + + ggml_tensor * routed_out = build_moe_ffn(cur, + layer.ffn_gate_inp, + layer.ffn_up_exps, + layer.ffn_gate_exps, + layer.ffn_down_exps, + layer.ffn_exp_probs_b, + n_expert, n_expert_used, + LLM_FFN_SILU, hparams.expert_weights_norm, + hparams.expert_weights_scale, + (llama_expert_gating_func_type) hparams.expert_gating_func, + il); + cb(routed_out, "mtp_ffn_moe_out", il); + + ggml_tensor * shared_out = build_ffn(cur, + layer.ffn_up_shexp, nullptr, nullptr, + layer.ffn_gate_shexp, nullptr, nullptr, + layer.ffn_down_shexp, nullptr, nullptr, + nullptr, + LLM_FFN_SILU, LLM_FFN_PAR, il); + cb(shared_out, "mtp_ffn_shexp_out", il); + + cur = ggml_add(ctx0, routed_out, shared_out); + cb(cur, "mtp_ffn_out", il); + + cur = ggml_add(ctx0, cur, ffn_inp); + cb(cur, "mtp_post_ffn", il); + + ggml_tensor * head_norm_w = layer.nextn.shared_head_norm + ? layer.nextn.shared_head_norm + : model.output_norm; + GGML_ASSERT(head_norm_w && "GLM4_MOE MTP: missing both nextn.shared_head_norm and output_norm"); + + cur = build_norm(cur, head_norm_w, nullptr, LLM_NORM_RMS, -1); + cb(cur, "h_nextn", -1); + res->t_h_nextn = cur; + + if (inp_out_ids) { + cur = ggml_get_rows(ctx0, cur, inp_out_ids); + } + cb(cur, "mtp_shared_head_norm", -1); + + ggml_tensor * head_w = layer.nextn.shared_head_head + ? layer.nextn.shared_head_head + : model.output; + ggml_tensor * head_s = layer.nextn.shared_head_head + ? layer.nextn.shared_head_head_s + : model.output_s; + GGML_ASSERT(head_w && "GLM4_MOE MTP: missing LM head (nextn.shared_head_head or model.output)"); + + cur = build_lora_mm(head_w, cur, head_s); + cb(cur, "result_output", -1); + + res->t_logits = cur; + ggml_build_forward_expand(gf, cur); +} + llama_model_glm4_moe::graph::graph(const llama_model & model, const llm_graph_params & params) : llm_graph_context(params) { const int64_t n_embd_head = hparams.n_embd_head_v(); @@ -154,8 +318,7 @@ llama_model_glm4_moe::graph::graph(const llama_model & model, const llm_graph_pa ggml_tensor * inp_out_ids = build_inp_out_ids(); - // Only process up to last layer (skip final NextN layer) - // Final layer tensors are loaded but not processed in forward pass + // NextN layers are processed by graph_mtp. for (int il = 0; il < n_layer; ++il) { ggml_tensor * inpSA = inpL; @@ -205,7 +368,7 @@ llama_model_glm4_moe::graph::graph(const llama_model & model, const llm_graph_pa model.layers[il].wo, NULL, model.layers[il].wo_s, Qcur, Kcur, Vcur, nullptr, nullptr, nullptr, 1.0f/sqrtf(float(n_embd_head)), il); } - if (il == n_layer - 1 && inp_out_ids) { + if (il == n_layer - 1 && inp_out_ids && (!cparams.embeddings_nextn || cparams.embeddings_nextn_masked)) { cur = ggml_get_rows(ctx0, cur, inp_out_ids); inpSA = ggml_get_rows(ctx0, inpSA, inp_out_ids); } @@ -265,6 +428,13 @@ llama_model_glm4_moe::graph::graph(const llama_model & model, const llm_graph_pa cur = inpL; cur = build_norm(cur, model.output_norm, NULL, LLM_NORM_RMS, -1); + cb(cur, "h_nextn", -1); + res->t_h_nextn = cur; + + if (cparams.embeddings_nextn && !cparams.embeddings_nextn_masked && inp_out_ids) { + cur = ggml_get_rows(ctx0, cur, inp_out_ids); + } + cb(cur, "result_norm", -1); res->t_embd = cur; diff --git a/src/models/models.h b/src/models/models.h index 157b05dc0..969429e3b 100644 --- a/src/models/models.h +++ b/src/models/models.h @@ -1412,6 +1412,10 @@ struct llama_model_glm4_moe : public llama_model_base { graph(const llama_model & model, const llm_graph_params & params); }; + struct graph_mtp : public llm_graph_context { + graph_mtp(const llama_model & model, const llm_graph_params & params); + }; + std::unique_ptr build_arch_graph(const llm_graph_params & params) const override; }; From bf0a29cc16066d313f6a8b52025620287a75cbc4 Mon Sep 17 00:00:00 2001 From: Aman Gupta Date: Mon, 24 Aug 2026 11:50:25 +0530 Subject: [PATCH 20/28] Deepseek 4: `-sm tensor` (#26490) * DSV4: sm tensor * set coarser granularity for head splits * fix dspark * add model saving for dsv4 + allow dflash to return on specific device * add comment about dsv4 seq_rm * simplify * add shared expert delayed allreduce * remove special test for dsv4 --- ggml/src/ggml-backend-meta.cpp | 212 +++++++++++++++++++++++++++++++-- src/llama-arch.cpp | 1 - src/llama-kv-cache-dsv4.cpp | 1 + src/llama-model-saver.cpp | 17 ++- src/llama-model.cpp | 70 ++++++++++- src/models/dflash.cpp | 7 +- tests/test-llama-archs.cpp | 54 ++++----- 7 files changed, 314 insertions(+), 48 deletions(-) diff --git a/ggml/src/ggml-backend-meta.cpp b/ggml/src/ggml-backend-meta.cpp index ded678e68..fe58ea3bb 100644 --- a/ggml/src/ggml-backend-meta.cpp +++ b/ggml/src/ggml-backend-meta.cpp @@ -592,7 +592,18 @@ static struct ggml_backend_meta_split_state ggml_backend_meta_get_split_state( GGML_ASSERT(split_states_equal(src_ss[0], src_ss[1])); return {assume_sync ? GGML_BACKEND_SPLIT_AXIS_MIRRORED : GGML_BACKEND_SPLIT_AXIS_PARTIAL, {0}, {1}, 1}; } - GGML_ABORT("fatal error"); + if (src_ss[0].axis == src_ss[1].axis && src_ss[0].axis >= GGML_BACKEND_SPLIT_AXIS_2 && + src_ss[0].axis < GGML_MAX_DIMS) { + GGML_ASSERT(split_states_equal(src_ss[0], src_ss[1])); + return src_ss[0]; + } + // batched matmul with the batches split across devices and a replicated activation + if (src_ss[0].axis >= GGML_BACKEND_SPLIT_AXIS_2 && src_ss[0].axis < GGML_MAX_DIMS && + src_ss[1].axis == GGML_BACKEND_SPLIT_AXIS_MIRRORED) { + return src_ss[0]; + } + GGML_ABORT("unsupported mul_mat split states: node=%s src0=%s axis=%d src1=%s axis=%d", + tensor->name, tensor->src[0]->name, (int) src_ss[0].axis, tensor->src[1]->name, (int) src_ss[1].axis); //return {GGML_BACKEND_SPLIT_AXIS_UNKNOWN, {0}, {1}, 1}; }; @@ -760,14 +771,33 @@ static struct ggml_backend_meta_split_state ggml_backend_meta_get_split_state( }; auto handle_flash_attn_ext = [&](const std::vector & src_ss) -> ggml_backend_meta_split_state { - GGML_ASSERT( src_ss[0].axis == GGML_BACKEND_SPLIT_AXIS_2); - GGML_ASSERT( src_ss[1].axis == GGML_BACKEND_SPLIT_AXIS_2); - GGML_ASSERT( src_ss[2].axis == GGML_BACKEND_SPLIT_AXIS_2); - GGML_ASSERT(tensor->src[4] == nullptr || src_ss[3].axis == GGML_BACKEND_SPLIT_AXIS_MIRRORED); + GGML_ASSERT(tensor->src[3] == nullptr || src_ss[3].axis == GGML_BACKEND_SPLIT_AXIS_MIRRORED); + + if (src_ss[0].axis == GGML_BACKEND_SPLIT_AXIS_MIRRORED) { + GGML_ASSERT(src_ss[1].axis == GGML_BACKEND_SPLIT_AXIS_MIRRORED); + GGML_ASSERT(src_ss[2].axis == GGML_BACKEND_SPLIT_AXIS_MIRRORED); + GGML_ASSERT(tensor->src[4] == nullptr || src_ss[4].axis == GGML_BACKEND_SPLIT_AXIS_MIRRORED); + return {GGML_BACKEND_SPLIT_AXIS_MIRRORED, {0}, {1}, 1}; + } + + GGML_ASSERT(src_ss[0].axis == GGML_BACKEND_SPLIT_AXIS_2); + const bool kv_split = src_ss[1].axis == GGML_BACKEND_SPLIT_AXIS_2 && + src_ss[2].axis == GGML_BACKEND_SPLIT_AXIS_2; + const bool kv_mirrored = src_ss[1].axis == GGML_BACKEND_SPLIT_AXIS_MIRRORED && + src_ss[2].axis == GGML_BACKEND_SPLIT_AXIS_MIRRORED; + GGML_ASSERT(kv_split || kv_mirrored); GGML_ASSERT(tensor->src[4] == nullptr || src_ss[4].axis == GGML_BACKEND_SPLIT_AXIS_0); return {GGML_BACKEND_SPLIT_AXIS_1, {0}, {1}, 1}; }; + auto handle_lightning_indexer = [&]( + const std::vector & src_ss) -> ggml_backend_meta_split_state { + for (size_t i = 0; i < 4; i++) { + GGML_ASSERT(src_ss[i].axis == GGML_BACKEND_SPLIT_AXIS_MIRRORED); + } + return {GGML_BACKEND_SPLIT_AXIS_MIRRORED, {0}, {1}, 1}; + }; + auto handle_ssm_conv = [&](const std::vector & src_ss) -> ggml_backend_meta_split_state { if (src_ss[0].axis == src_ss[1].axis) { if (src_ss[0].axis == GGML_BACKEND_SPLIT_AXIS_0) { @@ -938,7 +968,7 @@ static struct ggml_backend_meta_split_state ggml_backend_meta_get_split_state( split_state = handle_rope(src_ss); } break; case GGML_OP_ROPE_BACK: { - split_state = handle_generic(src_ss, /*scalar_only =*/ true); + split_state = handle_rope(src_ss); } break; case GGML_OP_CLAMP: { split_state = handle_generic(src_ss, /*scalar_only =*/ false); @@ -1002,6 +1032,9 @@ static struct ggml_backend_meta_split_state ggml_backend_meta_get_split_state( case GGML_OP_GATED_DELTA_NET: { split_state = handle_gated_delta_net(src_ss); } break; + case GGML_OP_LIGHTNING_INDEXER: { + split_state = handle_lightning_indexer(src_ss); + } break; case GGML_OP_DSV4_HC_COMB: case GGML_OP_DSV4_HC_PRE: case GGML_OP_DSV4_HC_POST: { @@ -1086,13 +1119,14 @@ static struct ggml_backend_meta_split_state ggml_backend_meta_get_split_state( if (buf_ctx->debug > 0) { std::string srcs_info; for (size_t i = 0; i < GGML_MAX_SRC; i++) { - if (tensor->src[i] == nullptr) { + if (tensor->src[i] == nullptr || tensor->src[i] == tensor) { continue; } if (!srcs_info.empty()) { srcs_info += ", "; } - const ggml_backend_meta_split_state split_state = ggml_backend_meta_get_split_state(tensor->src[0], true); + const ggml_backend_meta_split_state split_state = + ggml_backend_meta_get_split_state(tensor->src[i], true); GGML_ASSERT(split_state.n_segments == 1); const char * axis_name = ggml_backend_meta_split_axis_name(split_state.axis); std::string ne_info; @@ -1271,6 +1305,108 @@ static enum ggml_status ggml_backend_meta_buffer_init_tensor(ggml_backend_buffer return ggml_backend_meta_buffer_init_tensor_impl(buf_ctx->get_simple_tensor_container(tensor), tensor); } +static void ggml_backend_meta_buffer_memset_tensor( + ggml_backend_buffer_t buffer, ggml_tensor * tensor, uint8_t value, size_t offset, size_t size) { + const size_t n_bufs = ggml_backend_meta_buffer_n_bufs(buffer); + const ggml_backend_meta_split_state split_state = + ggml_backend_meta_get_split_state(tensor, /*assume_sync =*/ false); + GGML_ASSERT(ggml_is_contiguous(tensor) || split_state.axis == GGML_BACKEND_SPLIT_AXIS_MIRRORED); + + if (split_state.n_segments != 1 || split_state.nr[0] != 1) { + GGML_ASSERT(split_state.axis >= 0 && split_state.axis < GGML_MAX_DIMS); + GGML_ASSERT(split_state.nr[0] != 0); + GGML_ASSERT(tensor->ne[3] == 1); + + std::vector simple_offsets(n_bufs, 0); + if (split_state.axis == GGML_BACKEND_SPLIT_AXIS_0) { + GGML_ASSERT(tensor->ne[2] == 1); + + const size_t row_stride = tensor->nb[1]; + GGML_ASSERT(offset % row_stride == 0); + GGML_ASSERT(size % row_stride == 0); + const int64_t row_start = offset / row_stride; + const int64_t row_count = size / row_stride; + GGML_ASSERT(row_start + row_count <= tensor->ne[1]); + + const int64_t blck_size = ggml_blck_size(tensor->type); + for (size_t s = 0; s < split_state.n_segments; s++) { + for (size_t r = 0; r < split_state.nr[s]; r++) { + for (size_t j = 0; j < n_bufs; j++) { + ggml_tensor * simple_tensor = ggml_backend_meta_buffer_simple_tensor(tensor, j); + GGML_ASSERT(split_state.ne[s*n_bufs + j] % blck_size == 0); + const size_t nbytes = split_state.ne[s*n_bufs + j]/blck_size * tensor->nb[0]; + for (int64_t row = 0; row < row_count; row++) { + ggml_backend_tensor_memset(simple_tensor, value, + simple_offsets[j] + (row_start + row)*simple_tensor->nb[1], nbytes); + } + simple_offsets[j] += nbytes; + } + } + } + return; + } + + GGML_ASSERT(split_state.axis == GGML_BACKEND_SPLIT_AXIS_1); + + const size_t row_stride = tensor->nb[2]; + GGML_ASSERT(offset % row_stride == 0); + GGML_ASSERT(size % row_stride == 0); + const int64_t row_start = offset / row_stride; + const int64_t row_count = size / row_stride; + GGML_ASSERT(row_start + row_count <= tensor->ne[2]); + + for (size_t s = 0; s < split_state.n_segments; s++) { + for (size_t r = 0; r < split_state.nr[s]; r++) { + for (size_t j = 0; j < n_bufs; j++) { + ggml_tensor * simple_tensor = ggml_backend_meta_buffer_simple_tensor(tensor, j); + const size_t nbytes = split_state.ne[s*n_bufs + j] * tensor->nb[1]; + for (int64_t row = 0; row < row_count; row++) { + ggml_backend_tensor_memset(simple_tensor, value, + simple_offsets[j] + (row_start + row)*simple_tensor->nb[2], nbytes); + } + simple_offsets[j] += nbytes; + } + } + } + return; + } + + switch (split_state.axis) { + case GGML_BACKEND_SPLIT_AXIS_0: + case GGML_BACKEND_SPLIT_AXIS_1: + case GGML_BACKEND_SPLIT_AXIS_2: { + const size_t chunk_size_full = tensor->nb[split_state.axis + 1]; + GGML_ASSERT(offset % chunk_size_full == 0); + GGML_ASSERT(size % chunk_size_full == 0); + const int64_t i_start = offset / chunk_size_full; + const int64_t i_stop = (offset + size) / chunk_size_full; + for (size_t j = 0; j < n_bufs; j++) { + ggml_tensor * simple_tensor = ggml_backend_meta_buffer_simple_tensor(tensor, j); + const size_t chunk_size = simple_tensor->nb[split_state.axis + 1]; + if (chunk_size == 0) { + continue; + } + for (int64_t i = i_start; i < i_stop; i++) { + ggml_backend_tensor_memset(simple_tensor, value, i*chunk_size, chunk_size); + } + } + } break; + case GGML_BACKEND_SPLIT_AXIS_PARTIAL: { + GGML_ASSERT(value == 0); + [[fallthrough]]; + } + case GGML_BACKEND_SPLIT_AXIS_MIRRORED: { + for (size_t j = 0; j < n_bufs; j++) { + ggml_tensor * simple_tensor = ggml_backend_meta_buffer_simple_tensor(tensor, j); + ggml_backend_tensor_memset(simple_tensor, value, offset, size); + } + } break; + default: { + GGML_ABORT("fatal error"); + } + } +} + static void ggml_backend_meta_buffer_set_tensor(ggml_backend_buffer_t buffer, ggml_tensor * tensor, const void * data, size_t offset, size_t size) { const size_t n_bufs = ggml_backend_meta_buffer_n_bufs(buffer); const ggml_backend_meta_split_state split_state = ggml_backend_meta_get_split_state(tensor, /*assume_sync =*/ false); @@ -1518,7 +1654,7 @@ static const ggml_backend_buffer_i ggml_backend_meta_buffer_iface = { /* .free_buffer = */ ggml_backend_meta_buffer_free_buffer, /* .get_base = */ ggml_backend_meta_buffer_get_base, /* .init_tensor = */ ggml_backend_meta_buffer_init_tensor, - /* .memset_tensor = */ nullptr, // TODO implement + /* .memset_tensor = */ ggml_backend_meta_buffer_memset_tensor, /* .set_tensor = */ ggml_backend_meta_buffer_set_tensor, /* .get_tensor = */ ggml_backend_meta_buffer_get_tensor, /* .set_tensor_2d = */ nullptr, @@ -1871,7 +2007,7 @@ static enum ggml_status ggml_backend_meta_graph_compute(ggml_backend_t backend, { // For MoE models it may make sense to delay the AllReduce in order to reduce I/O: - auto get_i_delayed = [&](const int i) -> int { + auto get_i_delayed_branch = [&](const int i) -> int { int id = i; // i_delayed int idr = i; // i_delayed return, last safe return value @@ -1971,6 +2107,62 @@ static enum ggml_status ggml_backend_meta_graph_compute(ggml_backend_t backend, return idr; }; + // AllReduce(a) + AllReduce(b) == AllReduce(a + b) for independent partial branches. + auto get_i_delayed = [&](const int i) -> int { + const int i_delayed = get_i_delayed_branch(i); + ggml_tensor * node = cgraph->nodes[i_delayed]; + + if (ggml_node_get_use_count(cgraph, i_delayed) != 1) { + return i_delayed; + } + + for (int id = i_delayed + 1; id < cgraph->n_nodes; id++) { + ggml_tensor * next = cgraph->nodes[id]; + if (next->view_src == node) { + return i_delayed; + } + for (int s = 0; s < GGML_MAX_SRC; s++) { + if (next->src[s] == node) { + return i_delayed; + } + } + + if (next->view_src != nullptr && next->view_src->op == GGML_OP_NONE && ggml_backend_buffer_is_host(next->view_src->buffer)) { + continue; + } + if (ggml_backend_meta_get_split_state(next, false).axis != GGML_BACKEND_SPLIT_AXIS_PARTIAL) { + continue; + } + + const int i_other = id; + const int i_other_delayed = get_i_delayed_branch(i_other); + ggml_tensor * other = cgraph->nodes[i_other_delayed]; + if (ggml_node_get_use_count(cgraph, i_other_delayed) != 1 || i_other_delayed + 1 >= cgraph->n_nodes) { + return i_delayed; + } + + ggml_tensor * sum = cgraph->nodes[i_other_delayed + 1]; + if (sum->op != GGML_OP_ADD || + !ggml_are_same_shape(node, other) || node->type != other->type || sum->type != node->type || + !((sum->src[0] == node && sum->src[1] == other) || + (sum->src[0] == other && sum->src[1] == node)) || + ggml_backend_meta_get_split_state(sum, false).axis != GGML_BACKEND_SPLIT_AXIS_MIRRORED) { + return i_delayed; + } + + for (size_t j = 0; j < n_backends; j++) { + auto & bcj = backend_ctx->backend_configs[j]; + const bool compute = bcj.nodes[i]->flags & GGML_TENSOR_FLAG_COMPUTE; + const bool compute_other = bcj.nodes[i_other]->flags & GGML_TENSOR_FLAG_COMPUTE; + if (compute != compute_other) { + return i_delayed; + } + } + return i_other_delayed + 1; + } + return i_delayed; + }; + int i_start = 0; for (int i = 0; i < cgraph->n_nodes; i++) { ggml_tensor * node = cgraph->nodes[i]; diff --git a/src/llama-arch.cpp b/src/llama-arch.cpp index 025f9fb54..eecf444fc 100644 --- a/src/llama-arch.cpp +++ b/src/llama-arch.cpp @@ -1060,7 +1060,6 @@ bool llm_arch_supports_sm_tensor(const llm_arch & arch) { case LLM_ARCH_OLMOE: case LLM_ARCH_DEEPSEEK2: case LLM_ARCH_DEEPSEEK32: - case LLM_ARCH_DEEPSEEK4: case LLM_ARCH_DOTS3NOTE: case LLM_ARCH_GLM_DSA: case LLM_ARCH_BITNET: diff --git a/src/llama-kv-cache-dsv4.cpp b/src/llama-kv-cache-dsv4.cpp index 58f78e438..948d08146 100644 --- a/src/llama-kv-cache-dsv4.cpp +++ b/src/llama-kv-cache-dsv4.cpp @@ -1737,6 +1737,7 @@ void llama_kv_cache_dsv4::clear_compressed(llama_seq_id seq_id, bool data) { kv->seq_rm(seq_id, -1, -1); if (data) { + //TODO: do not clear the kv-cache during `seq_rm`, ref: https://github.com/ggml-org/llama.cpp/pull/26490#discussion_r3798143663 for (uint32_t il : kv->get_layer_ids()) { dsv4_clear_tensor_stream(kv->get_k_storage(il), (uint32_t) seq_id); } diff --git a/src/llama-model-saver.cpp b/src/llama-model-saver.cpp index 2eb5b7aaf..9adaa93f6 100644 --- a/src/llama-model-saver.cpp +++ b/src/llama-model-saver.cpp @@ -296,11 +296,18 @@ void llama_model_saver::add_kv_from_model() { add_kv(LLM_KV_ATTENTION_OUTPUT_GROUP_COUNT, hparams.dsv4_o_group_count); add_kv(LLM_KV_ATTENTION_OUTPUT_LORA_RANK, hparams.dsv4_o_lora_rank); add_kv(LLM_KV_ATTENTION_COMPRESS_ROPE_FREQ_BASE, hparams.dsv4_compress_rope_base); - add_kv(LLM_KV_ATTENTION_COMPRESS_RATIOS, hparams.dsv4_compress_ratios, true); - add_kv(LLM_KV_HYPER_CONNECTION_COUNT, hparams.dsv4_hc_mult); + if (model->arch == LLM_ARCH_DEEPSEEK4 || hparams.dsv4_hc_mult > 0) { + // the loader requires one compress ratio per layer, including nextn layers + const std::vector compress_ratios( + hparams.dsv4_compress_ratios.begin(), hparams.dsv4_compress_ratios.begin() + hparams.n_layer_all); + add_kv(LLM_KV_ATTENTION_COMPRESS_RATIOS, compress_ratios); + } else { + add_kv(LLM_KV_ATTENTION_COMPRESS_RATIOS, hparams.dsv4_compress_ratios, true); + } + add_kv(LLM_KV_HYPER_CONNECTION_COUNT, hparams.dsv4_hc_mult); add_kv(LLM_KV_HYPER_CONNECTION_SINKHORN_ITERATIONS, hparams.dsv4_hc_sinkhorn_iters); - add_kv(LLM_KV_HYPER_CONNECTION_EPSILON, hparams.dsv4_hc_eps); - add_kv(LLM_KV_HASH_LAYER_COUNT, hparams.dsv4_hash_layer_count); + add_kv(LLM_KV_HYPER_CONNECTION_EPSILON, hparams.dsv4_hc_eps); + add_kv(LLM_KV_HASH_LAYER_COUNT, hparams.dsv4_hash_layer_count); const float rope_scaling_factor = hparams.rope_freq_scale_train == 1.0f ? 0.0f : 1.0f/hparams.rope_freq_scale_train; @@ -425,6 +432,8 @@ void llama_model_saver::add_tensors_from_model() { add_tensor(model->output_s); add_tensor(model->output_in_s); add_tensor(model->output_res_score); + add_tensor(model->nextn_proj_pre); + add_tensor(model->nextn_proj_post); add_tensor(model->cls); add_tensor(model->cls_b); add_tensor(model->cls_out); diff --git a/src/llama-model.cpp b/src/llama-model.cpp index 33f5661b2..c34700ff5 100644 --- a/src/llama-model.cpp +++ b/src/llama-model.cpp @@ -365,6 +365,8 @@ struct ggml_backend_meta_split_state llama_meta_device_get_split_state(const str const llama_meta_device_get_split_state_userdata * ud = (const llama_meta_device_get_split_state_userdata *) userdata; const llama_hparams & hparams = ud->model->hparams; const std::string tensor_name = tensor->name; + const bool is_dsv4 = ud->model->arch == LLM_ARCH_DEEPSEEK4 || + (ud->model->arch == LLM_ARCH_DFLASH && hparams.dsv4_hc_mult > 0); static const std::regex pattern_q_weight ("blk\\.\\d*\\.attn_q.weight"); static const std::regex pattern_kv_weight ("blk\\.\\d*\\.attn_(k|v).weight"); @@ -374,9 +376,13 @@ struct ggml_backend_meta_split_state llama_meta_device_get_split_state(const str static const std::regex pattern_qkv_bias ("blk\\.\\d*\\.attn_qkv.bias"); static const std::regex pattern_qk_norm ("blk\\.\\d*\\.attn_(q|k)_norm\\.weight"); static const std::regex pattern_kv_cache ("cache_(k|v)_l\\d*"); + static const std::regex pattern_dsv4_state ("dsv4_(csa|hca|lid)_state_(kv|score)_l\\d*"); static const std::regex pattern_attn_sinks ("blk\\.\\d*\\.attn_sinks.weight"); static const std::regex pattern_attn_out_weight ("blk\\.\\d*\\.attn_output.weight"); static const std::regex pattern_attn_out_bias ("blk\\.\\d*\\.attn_output.bias"); + static const std::regex pattern_attn_out_a_weight("blk\\.\\d*\\.attn_output_a\\.weight"); + static const std::regex pattern_attn_out_b_weight("blk\\.\\d*\\.attn_output_b\\.weight"); + static const std::regex pattern_attn_q_b_weight ("blk\\.\\d*\\.attn_q_b\\.weight"); static const std::regex pattern_attn_gate_weight("blk\\.\\d*\\.attn_gate.weight"); static const std::regex pattern_ssm_dt ("blk\\.\\d*\\.ssm_dt.bias"); @@ -395,8 +401,11 @@ struct ggml_backend_meta_split_state llama_meta_device_get_split_state(const str static const std::regex pattern_ffn_gate_bias ("blk\\.\\d*\\.ffn_gate(_exps)?.bias"); static const std::regex pattern_ffn_gate_up_weight("blk\\.\\d*\\.ffn_gate_up(_exps)?.weight"); static const std::regex pattern_ffn_down_weight ("blk\\.\\d*\\.ffn_down(_exps)?.weight"); - static const std::regex pattern_ffn_down_bias ("blk\\.\\d*\\.ffn_down.bias"); - static const std::regex pattern_ffn_down_exps_bias("blk\\.\\d*\\.ffn_down_exps.bias"); + static const std::regex pattern_ffn_down_bias ("blk\\.\\d*\\.ffn_down.bias"); + static const std::regex pattern_ffn_down_exps_bias ("blk\\.\\d*\\.ffn_down_exps.bias"); + static const std::regex pattern_ffn_up_shexp_weight ("blk\\.\\d*\\.ffn_up_shexp.weight"); + static const std::regex pattern_ffn_gate_shexp_weight ("blk\\.\\d*\\.ffn_gate_shexp.weight"); + static const std::regex pattern_ffn_down_shexp_weight ("blk\\.\\d*\\.ffn_down_shexp.weight"); static const std::regex pattern_output_weight("output\\.weight"); static const std::regex pattern_output_bias ("output\\.bias"); @@ -453,6 +462,32 @@ struct ggml_backend_meta_split_state llama_meta_device_get_split_state(const str }; auto get_tensor_config = [&]() -> tensor_config { + if (is_dsv4) { + if (std::regex_match(tensor_name, pattern_kv_cache) || + std::regex_match(tensor_name, pattern_dsv4_state)) { + return get_tensor_config_impl(GGML_BACKEND_SPLIT_AXIS_MIRRORED); + } + if (std::regex_match(tensor_name, pattern_attn_sinks)) { + return get_tensor_config_impl(GGML_BACKEND_SPLIT_AXIS_0, "attn_output_a.weight"); + } + if (std::regex_match(tensor_name, pattern_attn_q_b_weight)) { + return get_tensor_config_impl(GGML_BACKEND_SPLIT_AXIS_1, "attn_output_a.weight"); + } + if (std::regex_match(tensor_name, pattern_attn_out_a_weight)) { + return get_tensor_config_impl(GGML_BACKEND_SPLIT_AXIS_2); + } + if (std::regex_match(tensor_name, pattern_attn_out_b_weight)) { + return get_tensor_config_impl(GGML_BACKEND_SPLIT_AXIS_0); + } + if (std::regex_match(tensor_name, pattern_ffn_up_shexp_weight) || + std::regex_match(tensor_name, pattern_ffn_gate_shexp_weight)) { + return get_tensor_config_impl(GGML_BACKEND_SPLIT_AXIS_1, "ffn_down_shexp.weight"); + } + if (std::regex_match(tensor_name, pattern_ffn_down_shexp_weight)) { + return get_tensor_config_impl(GGML_BACKEND_SPLIT_AXIS_0, "ffn_down_shexp.weight"); + } + } + // standard attention if (std::regex_match(tensor_name, pattern_q_weight) || std::regex_match(tensor_name, pattern_kv_weight)) { return get_tensor_config_impl(GGML_BACKEND_SPLIT_AXIS_1, "attn_output.weight", "ssm_out.weight"); @@ -525,6 +560,9 @@ struct ggml_backend_meta_split_state llama_meta_device_get_split_state(const str // output if (std::regex_match(tensor_name, pattern_output_weight)) { + if (is_dsv4) { + return get_tensor_config_impl(GGML_BACKEND_SPLIT_AXIS_MIRRORED); + } return get_tensor_config_impl(GGML_BACKEND_SPLIT_AXIS_1); } if (std::regex_match(tensor_name, pattern_output_bias)) { @@ -649,8 +687,30 @@ struct ggml_backend_meta_split_state llama_meta_device_get_split_state(const str const int64_t granularity_head = granularity_q / hparams.n_embd_head_k(il); // for tensors with one value per head if (std::regex_match(tensor_name, pattern_attn_sinks)) { GGML_ASSERT(segments.size() == 1); + if (is_dsv4) { + return {hparams.n_head(il) / hparams.dsv4_o_group_count}; + } return {granularity_head}; } + + if (is_dsv4) { + if (std::regex_match(tensor_name, pattern_attn_q_b_weight)) { + GGML_ASSERT(segments.size() == 1); + // the grouped output projection requires each device to hold whole groups of heads + const int64_t n_head_group = hparams.n_head(il) / hparams.dsv4_o_group_count; + return {n_head_group * hparams.n_embd_head_k(il)}; + } + if (std::regex_match(tensor_name, pattern_attn_out_a_weight)) { + GGML_ASSERT(segments.size() == 1); + return {1}; + } + if (std::regex_match(tensor_name, pattern_attn_out_b_weight)) { + GGML_ASSERT(segments.size() == 1); + // the boundaries must align with wo_a's per-group split, so quant blocks must not straddle groups + GGML_ASSERT(hparams.dsv4_o_lora_rank % blck_size == 0); + return {hparams.dsv4_o_lora_rank}; + } + } if (std::regex_match(tensor_name, pattern_q_weight) || std::regex_match(tensor_name, pattern_q_bias)) { GGML_ASSERT(segments.size() == 1); // some models have Q gate tensors, for those cases the granularity needs to be doubled: @@ -687,7 +747,11 @@ struct ggml_backend_meta_split_state llama_meta_device_get_split_state(const str // FFN if (std::regex_match(tensor_name, pattern_ffn_up_weight) || std::regex_match(tensor_name, pattern_ffn_up_bias) || std::regex_match(tensor_name, pattern_ffn_gate_weight) || std::regex_match(tensor_name, pattern_ffn_gate_bias) || - std::regex_match(tensor_name, pattern_ffn_gate_up_weight) || std::regex_match(tensor_name, pattern_ffn_down_weight)) { + std::regex_match(tensor_name, pattern_ffn_gate_up_weight) || + std::regex_match(tensor_name, pattern_ffn_down_weight) || + std::regex_match(tensor_name, pattern_ffn_up_shexp_weight) || + std::regex_match(tensor_name, pattern_ffn_gate_shexp_weight) || + std::regex_match(tensor_name, pattern_ffn_down_shexp_weight)) { const int64_t blck_size_perf = std::lcm(blck_size, 128); GGML_ASSERT(segments.size() == 1); return {blck_size_perf}; diff --git a/src/models/dflash.cpp b/src/models/dflash.cpp index d3c919b35..ff40c16b2 100644 --- a/src/models/dflash.cpp +++ b/src/models/dflash.cpp @@ -117,6 +117,10 @@ void llama_model_dflash::load_arch_tensors(llama_model_loader &) { output_norm_enc = create_tensor(tn(LLM_TENSOR_ENC_OUTPUT_NORM, "weight"), { n_embd }, 0); // encoder hidden_norm (after fc) output_norm = create_tensor(tn(LLM_TENSOR_OUTPUT_NORM, "weight"), { n_embd }, 0); // decoder final norm + // optional: reduced-vocab drafts ship their own lm head, full-vocab drafts can share the target's via ctx_other + // a draft with its own embeddings + head references no target tensors and can run on devices the target does not use (e.g. -devd with a tensor-split target) + output = create_tensor(tn(LLM_TENSOR_OUTPUT, "weight"), { n_embd, n_vocab_draft }, TENSOR_NOT_REQUIRED); + if (hparams.dsv4_hc_mult > 0) { const int64_t q_lora_rank = hparams.n_lora_q; const int64_t n_ff_exp = hparams.n_ff_exp; @@ -167,9 +171,6 @@ void llama_model_dflash::load_arch_tensors(llama_model_loader &) { return; } - // optional: reduced-vocab drafts ship their own, full-vocab drafts share the target's via ctx_other - output = create_tensor(tn(LLM_TENSOR_OUTPUT, "weight"), { n_embd, n_vocab_draft }, TENSOR_NOT_REQUIRED); - for (int i = 0; i < n_layer; ++i) { auto & layer = layers[i]; diff --git a/tests/test-llama-archs.cpp b/tests/test-llama-archs.cpp index dff8c4668..83922c53b 100644 --- a/tests/test-llama-archs.cpp +++ b/tests/test-llama-archs.cpp @@ -102,10 +102,11 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) { n_ff = 96; n_layer = 22; // hparams.n_layer_kv_from_start = 20 is hardcoded } else if (arch == LLM_ARCH_DEEPSEEK4) { - n_embd = 128; - n_head = 1; - n_ff = 192; - n_layer = 3; // uncompressed + csa + hca, one layer of each ratio kind + // head size 64 so that GPU flash attention kernels support the model + n_embd = 512; + n_head = 8; + n_ff = 1024; + n_layer = 4; } else if (arch == LLM_ARCH_STEP35 || arch == LLM_ARCH_LAGUNA) { n_embd = 160; // exercise per-head tensor split granularity with head size 80 } else if (arch == LLM_ARCH_QWEN3 || arch == LLM_ARCH_MUSE_GLIMMER || arch == LLM_ARCH_AFMOE) { @@ -175,11 +176,15 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) { ms.add_kv(LLM_KV_ATTENTION_HEAD_COUNT_KV, n_head_per_layer); } else { ms.add_kv(LLM_KV_ATTENTION_HEAD_COUNT, n_head); - ms.add_kv(LLM_KV_ATTENTION_HEAD_COUNT_KV, n_head_kv); + ms.add_kv(LLM_KV_ATTENTION_HEAD_COUNT_KV, arch == LLM_ARCH_DEEPSEEK4 ? uint32_t(1) : n_head_kv); } ms.add_kv(LLM_KV_ATTENTION_MAX_ALIBI_BIAS, 8.0f); - if (arch == LLM_ARCH_DEEPSEEK2 + if (arch == LLM_ARCH_DEEPSEEK4) { + ms.add_kv(LLM_KV_ATTENTION_KEY_LENGTH, n_embd_head); + ms.add_kv(LLM_KV_ATTENTION_VALUE_LENGTH, n_embd_head); + ms.add_kv(LLM_KV_ROPE_DIMENSION_COUNT, n_embd_head/2); + } else if (arch == LLM_ARCH_DEEPSEEK2 || arch == LLM_ARCH_DEEPSEEK32 || arch == LLM_ARCH_GLM_DSA || arch == LLM_ARCH_DOTS3NOTE @@ -208,10 +213,6 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) { } ms.add_kv(LLM_KV_ATTENTION_INDEXER_TYPES, indexer_types); } - } else if (arch == LLM_ARCH_DEEPSEEK4) { - ms.add_kv(LLM_KV_ATTENTION_KEY_LENGTH, uint32_t(128)); - ms.add_kv(LLM_KV_ATTENTION_VALUE_LENGTH, uint32_t(128)); - ms.add_kv(LLM_KV_ROPE_DIMENSION_COUNT, uint32_t(64)); } else if (arch == LLM_ARCH_MINIMAX_M3) { // partial rotary: n_rot must not exceed the indexer key length (64) ms.add_kv(LLM_KV_ROPE_DIMENSION_COUNT, uint32_t(64)); @@ -221,7 +222,7 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) { ms.add_kv(LLM_KV_ATTENTION_LAYERNORM_RMS_EPS, 1e-5f); ms.add_kv(LLM_KV_ATTENTION_GROUPNORM_EPS, 1e-5f); ms.add_kv(LLM_KV_ATTENTION_GROUPNORM_GROUPS, uint32_t(8)); - ms.add_kv(LLM_KV_ATTENTION_Q_LORA_RANK, uint32_t(512)); + ms.add_kv(LLM_KV_ATTENTION_Q_LORA_RANK, arch == LLM_ARCH_DEEPSEEK4 ? uint32_t(64) : uint32_t(512)); ms.add_kv(LLM_KV_ATTENTION_KV_LORA_RANK, uint32_t(512)); ms.add_kv(LLM_KV_ATTENTION_RELATIVE_BUCKETS_COUNT, uint32_t(8)); ms.add_kv(LLM_KV_ATTENTION_SLIDING_WINDOW, n_ctx/8); @@ -248,26 +249,26 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) { // MSA requires one indexer head per GQA (KV) head, unlike the DSA archs where the // indexer head count is independent of the main attention head count. - if (arch == LLM_ARCH_DEEPSEEK4) { - ms.add_kv(LLM_KV_EXPERT_WEIGHTS_SCALE, 2.5f); - ms.add_kv(LLM_KV_EXPERT_WEIGHTS_NORM, true); - ms.add_kv(LLM_KV_SWIGLU_CLAMP_EXP, 7.0f); - ms.add_kv(LLM_KV_ATTENTION_OUTPUT_GROUP_COUNT, uint32_t(1)); - ms.add_kv(LLM_KV_ATTENTION_OUTPUT_LORA_RANK, uint32_t(64)); - ms.add_kv(LLM_KV_ATTENTION_COMPRESS_ROPE_FREQ_BASE, 10000.0f); - ms.add_kv(LLM_KV_HYPER_CONNECTION_COUNT, uint32_t(4)); - ms.add_kv(LLM_KV_HYPER_CONNECTION_SINKHORN_ITERATIONS, uint32_t(4)); - ms.add_kv(LLM_KV_HYPER_CONNECTION_EPSILON, 1e-6f); - ms.add_kv(LLM_KV_HASH_LAYER_COUNT, uint32_t(0)); - ms.add_kv(LLM_KV_ATTENTION_COMPRESS_RATIOS, std::vector({0, 4, 128})); - } - - ms.add_kv(LLM_KV_ATTENTION_INDEXER_HEAD_COUNT, arch == LLM_ARCH_MINIMAX_M3 ? n_head : uint32_t(1)); + ms.add_kv(LLM_KV_ATTENTION_INDEXER_HEAD_COUNT, arch == LLM_ARCH_MINIMAX_M3 || arch == LLM_ARCH_DEEPSEEK4 ? n_head : uint32_t(1)); ms.add_kv(LLM_KV_ATTENTION_INDEXER_KEY_LENGTH, uint32_t(64)); ms.add_kv(LLM_KV_ATTENTION_INDEXER_TOP_K, uint32_t(8)); ms.add_kv(LLM_KV_ATTENTION_INDEXER_BLOCK_SIZE, uint32_t(4)); ms.add_kv(LLM_KV_ATTENTION_INDEXER_LOCAL_BLOCKS, uint32_t(1)); ms.add_kv(LLM_KV_ROPE_DIMENSION_SECTIONS, std::vector({n_embd_head/4, n_embd_head/4, n_embd_head/4, n_embd_head/4})); + + if (arch == LLM_ARCH_DEEPSEEK4) { + ms.add_kv(LLM_KV_ATTENTION_OUTPUT_GROUP_COUNT, uint32_t(8)); + ms.add_kv(LLM_KV_ATTENTION_OUTPUT_LORA_RANK, uint32_t(32)); + ms.add_kv(LLM_KV_ATTENTION_COMPRESS_RATIOS, std::vector({0, 0, 4, 128})); + ms.add_kv(LLM_KV_ATTENTION_COMPRESS_ROPE_FREQ_BASE, 160000.0f); + ms.add_kv(LLM_KV_HYPER_CONNECTION_COUNT, uint32_t(4)); + ms.add_kv(LLM_KV_HYPER_CONNECTION_SINKHORN_ITERATIONS, uint32_t(2)); + ms.add_kv(LLM_KV_HYPER_CONNECTION_EPSILON, 1.0e-6f); + ms.add_kv(LLM_KV_HASH_LAYER_COUNT, uint32_t(0)); + ms.add_kv(LLM_KV_SWIGLU_CLAMP_EXP, 10.0f); + ms.add_kv(LLM_KV_EXPERT_WEIGHTS_SCALE, 1.0f); + ms.add_kv(LLM_KV_EXPERT_WEIGHTS_NORM, true); + } ms.add_kv(LLM_KV_TOKENIZER_MODEL, "no_vocab"); // ms.add_kv(LLM_KV_DENSE_2_FEAT_OUT, n_embd); // ms.add_kv(LLM_KV_DENSE_3_FEAT_IN, n_embd); @@ -504,7 +505,6 @@ static bool arch_supported(const llm_arch arch) { if (arch == LLM_ARCH_DEEPSEEK2OCR) { return false; } - // FIXME: these hit scheduler/view-backed-output issues with WebGPU on CI. #ifdef GGML_USE_WEBGPU if (arch == LLM_ARCH_DEEPSEEK32 || arch == LLM_ARCH_GLM_DSA) { From a130532ae1c4c54daaae5527795f5b19c184f269 Mon Sep 17 00:00:00 2001 From: Prabhsimran Singh Date: Mon, 24 Aug 2026 11:55:11 +0530 Subject: [PATCH 21/28] mamba2 : Flatten in/out projections to dispatch GEMM instead of GEMV (#27513) * mamba2 : flatten mamba2 in/out projections to dispatch gemm instead of gemv * mamba2 : remove redundant output reshape --- src/models/mamba-base.cpp | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/models/mamba-base.cpp b/src/models/mamba-base.cpp index 1f994ae0a..03ee3805b 100644 --- a/src/models/mamba-base.cpp +++ b/src/models/mamba-base.cpp @@ -182,13 +182,14 @@ ggml_tensor * llm_build_mamba_base::build_mamba2_layer(llm_graph_input_rs * inp, ggml_tensor * conv = build_rs(inp, conv_states_all, hparams.n_embd_r(), n_seqs); conv = ggml_reshape_3d(ctx0, conv, d_conv - 1, d_inner + 2 * n_group * d_state, n_seqs); - // {n_embd, n_tokens} => {n_embd, n_seq_tokens, n_seqs} - cur = ggml_reshape_3d(ctx0, cur, cur->ne[0], n_seq_tokens, n_seqs); - // d_in_proj = 2 * self.d_inner + 2 * self.ngroups * self.d_state + self.nheads - // {n_embd, d_in_proj} @ {n_embd, n_seq_tokens, n_seqs} => {d_in_proj, n_seq_tokens, n_seqs} + // Keep the projection 2D: with a {n_embd, 1, n_seqs} batch the CUDA backend + // dispatches a column-batched GEMV for what is a large dense GEMM. + // {n_embd, d_in_proj} @ {n_embd, n_tokens} => {d_in_proj, n_tokens} ggml_tensor * zxBCdt = build_lora_mm(model.layers[il].ssm_in, cur, model.layers[il].ssm_in_s); + // {d_in_proj, n_tokens} => {d_in_proj, n_seq_tokens, n_seqs} + zxBCdt = ggml_reshape_3d(ctx0, zxBCdt, zxBCdt->ne[0], n_seq_tokens, n_seqs); // split the above in three ggml_tensor * z = ggml_view_4d(ctx0, zxBCdt, head_dim, n_head, n_seq_tokens, n_seqs, head_dim * zxBCdt->nb[0], @@ -290,15 +291,12 @@ ggml_tensor * llm_build_mamba_base::build_mamba2_layer(llm_graph_input_rs * inp, y = build_norm(y, model.layers[il].ssm_norm, NULL, LLM_NORM_RMS, il); } - y = ggml_reshape_3d(ctx0, y, d_inner, n_seq_tokens, n_seqs); + y = ggml_reshape_2d(ctx0, y, d_inner, n_seq_tokens * n_seqs); - // {d_inner, n_embd} @ {d_inner, n_seq_tokens, n_seqs} => {n_embd, n_seq_tokens, n_seqs} + // {d_inner, n_embd} @ {d_inner, n_tokens} => {n_embd, n_tokens} cur = build_lora_mm(model.layers[il].ssm_out, y, model.layers[il].ssm_out_s); } - // {n_embd, n_seq_tokens, n_seqs} => {n_embd, n_tokens} - cur = ggml_reshape_2d(ctx0, cur, cur->ne[0], n_seq_tokens * n_seqs); cb(cur, "mamba_out", il); - return cur; } From 6036c635e2d1e9aeca11d01a84188c9db9ea1e2a Mon Sep 17 00:00:00 2001 From: Georgi Gerganov Date: Mon, 24 Aug 2026 10:43:04 +0300 Subject: [PATCH 22/28] ggml : fix ggml_clamp (#27644) * ggml : fix ggml_clamp * cont : update ggml-alloc --- ggml/include/ggml.h | 21 ++++++++++------- ggml/src/ggml-alloc.c | 1 + ggml/src/ggml.c | 54 ++++++++++++++++++++++++++++--------------- 3 files changed, 49 insertions(+), 27 deletions(-) diff --git a/ggml/include/ggml.h b/ggml/include/ggml.h index 32462d79a..5f6774a63 100644 --- a/ggml/include/ggml.h +++ b/ggml/include/ggml.h @@ -1724,6 +1724,19 @@ extern "C" { struct ggml_tensor * a, int n_past); + GGML_API struct ggml_tensor * ggml_clamp( + struct ggml_context * ctx, + struct ggml_tensor * a, + float min, + float max); + + // in-place, returns view(a) + GGML_API struct ggml_tensor * ggml_clamp_inplace( + struct ggml_context * ctx, + struct ggml_tensor * a, + float min, + float max); + GGML_API struct ggml_tensor * ggml_soft_max( struct ggml_context * ctx, struct ggml_tensor * a); @@ -1990,14 +2003,6 @@ extern "C" { struct ggml_tensor * a, int n_offs); - // clamp - // in-place, returns view(a) - GGML_API struct ggml_tensor * ggml_clamp( - struct ggml_context * ctx, - struct ggml_tensor * a, - float min, - float max); - // im2col // converts data into a format that effectively results in a convolution when combined with matrix multiplication GGML_API struct ggml_tensor * ggml_im2col( diff --git a/ggml/src/ggml-alloc.c b/ggml/src/ggml-alloc.c index 3bda9abbe..a71838eaf 100644 --- a/ggml/src/ggml-alloc.c +++ b/ggml/src/ggml-alloc.c @@ -40,6 +40,7 @@ bool ggml_op_can_inplace(enum ggml_op op) { case GGML_OP_SILU_BACK: case GGML_OP_RMS_NORM: case GGML_OP_RMS_NORM_BACK: + case GGML_OP_CLAMP: case GGML_OP_SOFT_MAX: case GGML_OP_SOFT_MAX_BACK: return true; diff --git a/ggml/src/ggml.c b/ggml/src/ggml.c index 1a60fec79..e0b615c07 100644 --- a/ggml/src/ggml.c +++ b/ggml/src/ggml.c @@ -4042,6 +4042,41 @@ struct ggml_tensor * ggml_diag_mask_zero_inplace( return ggml_diag_mask_zero_impl(ctx, a, n_past, true); } +// ggml_clamp + +static struct ggml_tensor * ggml_clamp_impl( + struct ggml_context * ctx, + struct ggml_tensor * a, + float min, + float max, + bool inplace) { + struct ggml_tensor * result = inplace ? ggml_view_tensor(ctx, a) : ggml_dup_tensor(ctx, a); + + float params[] = { min, max }; + ggml_set_op_params(result, params, sizeof(params)); + + result->op = GGML_OP_CLAMP; + result->src[0] = a; + + return result; +} + +struct ggml_tensor * ggml_clamp( + struct ggml_context * ctx, + struct ggml_tensor * a, + float min, + float max) { + return ggml_clamp_impl(ctx, a, min, max, false); +} + +struct ggml_tensor * ggml_clamp_inplace( + struct ggml_context * ctx, + struct ggml_tensor * a, + float min, + float max) { + return ggml_clamp_impl(ctx, a, min, max, true); +} + // ggml_soft_max static struct ggml_tensor * ggml_soft_max_impl( @@ -4438,25 +4473,6 @@ struct ggml_tensor * ggml_rope_set_offset( return a; } -// ggml_clamp - -struct ggml_tensor * ggml_clamp( - struct ggml_context * ctx, - struct ggml_tensor * a, - float min, - float max) { - // TODO: when implement backward, fix this: - struct ggml_tensor * result = ggml_view_tensor(ctx, a); - - float params[] = { min, max }; - ggml_set_op_params(result, params, sizeof(params)); - - result->op = GGML_OP_CLAMP; - result->src[0] = a; - - return result; -} - static int64_t ggml_calc_conv_output_size(int64_t ins, int64_t ks, int s, int p, int d) { return (ins + 2 * p - d * (ks - 1) - 1) / s + 1; } From 985b14912bc9a3d8f27c1ac569e46f4092123722 Mon Sep 17 00:00:00 2001 From: Georgi Gerganov Date: Mon, 24 Aug 2026 10:49:20 +0300 Subject: [PATCH 23/28] ci : apply ccache-clear with older/min/dry-run to all ccache jobs (#27602) * ci : apply ccache-clear with older/min/dry-run to all ccache jobs Assisted-by: llama.cpp:DeepSeek-v4-Flash-0731 * ci : install gh in ccache-clear if missing (container jobs) The ccache-clear action relies on the gh CLI, which is not present in container-based jobs. Install it on demand so those jobs can clear caches. Assisted-by: llama.cpp:DeepSeek-v4-Flash-0731 * ci : install gh via apt repo in ccache-clear The install.sh script used previously is no longer served (404). Switch to the official GitHub CLI apt repository, which is still available. Assisted-by: llama.cpp:DeepSeek-v4-Flash-0731 * ci : pass --repo to gh cache commands in ccache-clear In container jobs gh cannot auto-detect the repository from git, so gh cache list/delete fail with 'failed to run git: not a git repository'. Pass the repository explicitly via --repo using GITHUB_REPOSITORY. Assisted-by: llama.cpp:DeepSeek-v4-Flash-0731 * ci : drop -new suffix from vulkan ccache key The -new suffix was only needed to force a fresh cache. With ccache-clear now evicting stale caches, the original key can be used again. The old ccache-vulkan-ubuntu-24.04-arm-new entries still match the ccache-clear key prefix and are cleaned up automatically. Assisted-by: llama.cpp:DeepSeek-v4-Flash-0731 * ci : fix ccache-clear date parsing on macOS (BSD date) macOS ships BSD date, which has no -d option. The older cutoff check was silently disabled there: 'date: illegal option -- d' errors in the log and the loop was only stopped by the min limit, risking deletion of caches not older than the cutoff (e.g. saved by a concurrent job). Parse the ISO-8601 timestamps with GNU date when available and fall back to BSD date otherwise (TZ=UTC, fractional seconds dropped). Assisted-by: llama.cpp:DeepSeek-v4-Flash-0731 * ci : extract ccache-clear logic into scripts/ccache-clear.sh The composite action now consists of a dedicated step that installs the GitHub CLI when missing (e.g. in container jobs) and a thin step that calls the new script. The script follows the make-release-checks.sh conventions (usage/env header, set -euo pipefail, CLI flags) and only checks that gh is available. The action inputs are unchanged, so the workflow steps are untouched. Assisted-by: llama.cpp:DeepSeek-v4-Flash-0731 * ci : remove unused apple ccaches --- .github/actions/ccache-clear/action.yml | 86 ++++++------------- .github/workflows/build-apple.yml | 44 +++++----- .github/workflows/build-cpu.yml | 12 ++- .github/workflows/build-cuda-ubuntu.yml | 30 +++++++ .github/workflows/build-opencl.yml | 10 +++ .github/workflows/build-openvino.yml | 10 +++ .github/workflows/build-sycl.yml | 20 +++++ .github/workflows/build-vulkan.yml | 32 +++++++- .github/workflows/build-wasm.yml | 10 +++ .github/workflows/build-webgpu.yml | 20 +++++ .github/workflows/hip-quality-check.yml | 10 +++ .github/workflows/server.yml | 20 +++++ scripts/ccache-clear.sh | 105 ++++++++++++++++++++++++ 13 files changed, 321 insertions(+), 88 deletions(-) create mode 100755 scripts/ccache-clear.sh diff --git a/.github/actions/ccache-clear/action.yml b/.github/actions/ccache-clear/action.yml index 2420045ed..fc5da4f6e 100644 --- a/.github/actions/ccache-clear/action.yml +++ b/.github/actions/ccache-clear/action.yml @@ -21,68 +21,30 @@ inputs: runs: using: "composite" steps: + - name: Install GitHub CLI if missing + shell: bash + run: | + # e.g. in container jobs, where it is not preinstalled + if ! command -v gh >/dev/null 2>&1; then + echo "GitHub CLI not found, installing..." + if ! command -v curl >/dev/null 2>&1; then + apt-get update >/dev/null 2>&1 || true + apt-get install -y curl >/dev/null 2>&1 || true + fi + mkdir -p -m 755 /etc/apt/keyrings + curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | tee /etc/apt/keyrings/githubcli-archive-keyring.gpg >/dev/null + chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list + apt-get update >/dev/null 2>&1 || true + apt-get install -y gh || { echo "Failed to install GitHub CLI (gh)" >&2; exit 1; } + fi + command -v gh >/dev/null 2>&1 || { echo "GitHub CLI (gh) is required but could not be installed" >&2; exit 1; } + - name: Clear caches shell: bash - env: - CLEAR_KEY: ${{ inputs.key }} - CLEAR_OLDER: ${{ inputs.older }} - CLEAR_MIN: ${{ inputs.min }} - CLEAR_DRY_RUN: ${{ inputs.dry-run }} run: | - # Convert a duration (e.g. 90m, 1h, 1d, plain seconds) to seconds - to_seconds() { - local val="$1" - [[ "$val" =~ ^[0-9]+$ ]] && { echo "$val"; return 0; } - local num="${val%?}" unit="${val: -1}" mult - [[ "$num" =~ ^[0-9]+$ ]] || return 1 - case "$unit" in - s) mult=1 ;; - m) mult=60 ;; - h) mult=3600 ;; - d) mult=86400 ;; - *) return 1 ;; - esac - echo $((num * mult)) - } - - [[ "$CLEAR_MIN" =~ ^[0-9]+$ ]] || { echo "Invalid min value: $CLEAR_MIN" >&2; exit 1; } - [[ "$CLEAR_DRY_RUN" =~ ^(true|false)$ ]] || { echo "Invalid dry-run value: $CLEAR_DRY_RUN" >&2; exit 1; } - - CACHES=$(gh cache list --key "ccache-$CLEAR_KEY" --json id,key,createdAt --jq '.[] | [.createdAt, .id, .key] | @tsv' 2>/dev/null | LC_ALL=C sort) - if [ -z "$CACHES" ]; then - echo "No caches found with key prefix: $CLEAR_KEY" - exit 0 - fi - - TOTAL=$(( $(wc -l <<< "$CACHES") )) - - echo "Found $TOTAL cache(s) with key prefix: $CLEAR_KEY (oldest first):" - while IFS=$'\t' read -r CREATED ID KEY; do - printf ' %s %s %s\n' "$CREATED" "$ID" "$KEY" - done <<< "$CACHES" - - CUTOFF="" - if [ -n "$CLEAR_OLDER" ]; then - OLDER_SECONDS=$(to_seconds "$CLEAR_OLDER") || { echo "Invalid older value: $CLEAR_OLDER (expected e.g. 90m, 1h, 1d)" >&2; exit 1; } - CUTOFF=$(( $(date +%s) - OLDER_SECONDS )) - fi - - # Caches are sorted oldest first - DELETED=0 - while IFS=$'\t' read -r CREATED ID KEY; do - if [ -n "$CUTOFF" ] && [ "$(date -d "$CREATED" +%s)" -ge "$CUTOFF" ]; then - echo "Rest are not older than $CLEAR_OLDER, stopping" - break - fi - if [ $((TOTAL - DELETED - 1)) -lt "$CLEAR_MIN" ]; then - echo "Keeping at least $CLEAR_MIN cache(s), stopping" - break - fi - if [ "$CLEAR_DRY_RUN" = "true" ]; then - echo "Would delete cache: $ID ($KEY)" - else - echo "Deleting cache: $ID ($KEY)" - gh cache delete "$ID" - fi - DELETED=$((DELETED + 1)) - done <<< "$CACHES" + bash scripts/ccache-clear.sh \ + --key "${{ inputs.key }}" \ + --older "${{ inputs.older }}" \ + --min "${{ inputs.min }}" \ + ${{ inputs.dry-run == 'true' && '--dry-run' || '' }} diff --git a/.github/workflows/build-apple.yml b/.github/workflows/build-apple.yml index 289e5144e..9a4a691d5 100644 --- a/.github/workflows/build-apple.yml +++ b/.github/workflows/build-apple.yml @@ -73,6 +73,16 @@ jobs: cd build ctest -L main -E "test-llama-archs" --verbose --timeout 900 + - name: ccache-clear + uses: ./.github/actions/ccache-clear + env: + GH_TOKEN: ${{ github.token }} + with: + key: apple-arm64 + older: 5m + min: 1 + dry-run: ${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }} + macos-latest-x64: runs-on: macos-15-intel @@ -109,6 +119,16 @@ jobs: cd build ctest -L main --verbose --timeout 900 + - name: ccache-clear + uses: ./.github/actions/ccache-clear + env: + GH_TOKEN: ${{ github.token }} + with: + key: apple-x64 + older: 5m + min: 1 + dry-run: ${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }} + macos-latest-ios-xcode: runs-on: macos-latest @@ -163,14 +183,6 @@ jobs: id: checkout uses: actions/checkout@v6 - # TODO: this likely does not do anything - if yes, remove it - - name: ccache - uses: ggml-org/ccache-action@v1.2.21 - with: - key: apple-tvos - evict-old-files: 1d - save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} - - name: Build id: cmake_build run: | @@ -196,14 +208,6 @@ jobs: id: checkout uses: actions/checkout@v6 - # TODO: this likely does not do anything - if yes, remove it - - name: ccache - uses: ggml-org/ccache-action@v1.2.21 - with: - key: apple-visionos - evict-old-files: 1d - save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} - - name: Build id: cmake_build run: | @@ -234,14 +238,6 @@ jobs: id: checkout uses: actions/checkout@v6 - # TODO: this likely does not do anything - if yes, remove it - - name: ccache - uses: ggml-org/ccache-action@v1.2.21 - with: - key: apple-swift - evict-old-files: 1d - save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} - - name: Download xcframework artifact uses: actions/download-artifact@v7 with: diff --git a/.github/workflows/build-cpu.yml b/.github/workflows/build-cpu.yml index f39304cab..b62fe55d6 100644 --- a/.github/workflows/build-cpu.yml +++ b/.github/workflows/build-cpu.yml @@ -125,7 +125,7 @@ jobs: GH_TOKEN: ${{ github.token }} with: key: cpu-${{ matrix.os }} - older: 1h + older: 5m min: 1 dry-run: ${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }} @@ -215,3 +215,13 @@ jobs: # cd build # $env:LLAMA_SKIP_TESTS_SLOW_ON_EMULATOR = 1 # & $sde -future -- ctest -L main -C Release --verbose --timeout 900 + + - name: ccache-clear + uses: ./.github/actions/ccache-clear + env: + GH_TOKEN: ${{ github.token }} + with: + key: cpu-windows-2025-${{ matrix.build }} + older: 5m + min: 1 + dry-run: ${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }} diff --git a/.github/workflows/build-cuda-ubuntu.yml b/.github/workflows/build-cuda-ubuntu.yml index 2528b1857..ed5164756 100644 --- a/.github/workflows/build-cuda-ubuntu.yml +++ b/.github/workflows/build-cuda-ubuntu.yml @@ -72,6 +72,16 @@ jobs: -DGGML_CUDA_CUB_3DOT2=ON cmake --build build + - name: ccache-clear + uses: ./.github/actions/ccache-clear + env: + GH_TOKEN: ${{ github.token }} + with: + key: cuda-ubuntu-24.04-cuda + older: 5m + min: 1 + dry-run: ${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }} + hip: runs-on: ubuntu-22.04 container: rocm/dev-ubuntu-22.04:6.1.2 @@ -103,6 +113,16 @@ jobs: -DGGML_HIP=ON cmake --build build --config Release -j $(nproc) + - name: ccache-clear + uses: ./.github/actions/ccache-clear + env: + GH_TOKEN: ${{ github.token }} + with: + key: cuda-ubuntu-22.04-hip + older: 5m + min: 1 + dry-run: ${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }} + musa: runs-on: ubuntu-22.04 container: mthreads/musa:rc4.3.0-devel-ubuntu22.04-amd64 @@ -131,3 +151,13 @@ jobs: cmake -B build -S . \ -DGGML_MUSA=ON time cmake --build build --config Release -j $(nproc) + + - name: ccache-clear + uses: ./.github/actions/ccache-clear + env: + GH_TOKEN: ${{ github.token }} + with: + key: cuda-ubuntu-22.04-musa + older: 5m + min: 1 + dry-run: ${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }} diff --git a/.github/workflows/build-opencl.yml b/.github/workflows/build-opencl.yml index 251b1f8d5..c0adc7e49 100644 --- a/.github/workflows/build-opencl.yml +++ b/.github/workflows/build-opencl.yml @@ -80,3 +80,13 @@ jobs: run: | cmake -S . -B build -G "Ninja Multi-Config" -D CMAKE_TOOLCHAIN_FILE=cmake/arm64-windows-llvm.cmake -DCMAKE_PREFIX_PATH="$env:RUNNER_TEMP/opencl-arm64-release" -DGGML_OPENCL=ON -DGGML_OPENCL_USE_ADRENO_KERNELS=ON -DLLAMA_BUILD_BORINGSSL=ON cmake --build build --config Release -j ${env:NUMBER_OF_PROCESSORS} + + - name: ccache-clear + uses: ./.github/actions/ccache-clear + env: + GH_TOKEN: ${{ github.token }} + with: + key: opencl-windows-2025-x64 + older: 5m + min: 1 + dry-run: ${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }} diff --git a/.github/workflows/build-openvino.yml b/.github/workflows/build-openvino.yml index ee4268f97..0316e7ad9 100644 --- a/.github/workflows/build-openvino.yml +++ b/.github/workflows/build-openvino.yml @@ -167,3 +167,13 @@ jobs: cd build ctest --test-dir ReleaseOV -L main -E "test-llama-archs|test-recurrent-state-rollback-nemotron-h" -C Release --verbose --timeout 3000 + + - name: ccache-clear + uses: ./.github/actions/ccache-clear + env: + GH_TOKEN: ${{ github.token }} + with: + key: openvino-windows-2022 + older: 5m + min: 1 + dry-run: ${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }} diff --git a/.github/workflows/build-sycl.yml b/.github/workflows/build-sycl.yml index deb0e5479..7beac8177 100644 --- a/.github/workflows/build-sycl.yml +++ b/.github/workflows/build-sycl.yml @@ -96,6 +96,16 @@ jobs: -DGGML_SYCL_F16=${{ matrix.fp16 }} time cmake --build build --config Release -j $(nproc) + - name: ccache-clear + uses: ./.github/actions/ccache-clear + env: + GH_TOKEN: ${{ github.token }} + with: + key: sycl-ubuntu-24-${{ matrix.build }} + older: 5m + min: 1 + dry-run: ${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }} + windows-latest-sycl: runs-on: windows-2022 @@ -139,3 +149,13 @@ jobs: - name: Build id: cmake_build run: examples/sycl/win-build-sycl.bat + + - name: ccache-clear + uses: ./.github/actions/ccache-clear + env: + GH_TOKEN: ${{ github.token }} + with: + key: sycl-windows-latest + older: 5m + min: 1 + dry-run: ${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }} diff --git a/.github/workflows/build-vulkan.yml b/.github/workflows/build-vulkan.yml index 15ff4b0af..74d1c6936 100644 --- a/.github/workflows/build-vulkan.yml +++ b/.github/workflows/build-vulkan.yml @@ -55,7 +55,7 @@ jobs: - name: ccache uses: ggml-org/ccache-action@v1.2.21 with: - key: vulkan-ubuntu-24.04-arm-new + key: vulkan-ubuntu-24.04-arm variant: ccache evict-old-files: 1d save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} @@ -73,6 +73,16 @@ jobs: run: | time cmake --build build -j $(nproc) + - name: ccache-clear + uses: ./.github/actions/ccache-clear + env: + GH_TOKEN: ${{ github.token }} + with: + key: vulkan-ubuntu-24.04-arm + older: 5m + min: 1 + dry-run: ${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }} + ubuntu-llvmpipe: runs-on: ubuntu-24.04 @@ -128,6 +138,16 @@ jobs: # test-backend-ops is too slow on llvmpipe, skip it ctest -L main -E test-backend-ops --verbose --timeout 900 + - name: ccache-clear + uses: ./.github/actions/ccache-clear + env: + GH_TOKEN: ${{ github.token }} + with: + key: vulkan-ubuntu-24.04-llvmpipe + older: 5m + min: 1 + dry-run: ${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }} + windows: runs-on: windows-2025 @@ -180,3 +200,13 @@ jobs: run: | cd build ctest -L main -C Release --verbose --timeout 900 + + - name: ccache-clear + uses: ./.github/actions/ccache-clear + env: + GH_TOKEN: ${{ github.token }} + with: + key: cpu-windows-2025-x64-vulkan + older: 5m + min: 1 + dry-run: ${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }} diff --git a/.github/workflows/build-wasm.yml b/.github/workflows/build-wasm.yml index aa7ae887d..2e4680f38 100644 --- a/.github/workflows/build-wasm.yml +++ b/.github/workflows/build-wasm.yml @@ -88,3 +88,13 @@ jobs: -DEMDAWNWEBGPU_DIR=emdawnwebgpu_pkg time cmake --build build-wasm --config Release --target test-backend-ops -j $(nproc) + + - name: ccache-clear + uses: ./.github/actions/ccache-clear + env: + GH_TOKEN: ${{ github.token }} + with: + key: webgpu-ubuntu-24.04-arm-wasm + older: 5m + min: 1 + dry-run: ${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }} diff --git a/.github/workflows/build-webgpu.yml b/.github/workflows/build-webgpu.yml index ed73c185a..b357851aa 100644 --- a/.github/workflows/build-webgpu.yml +++ b/.github/workflows/build-webgpu.yml @@ -101,6 +101,16 @@ jobs: cd build ctest -L main --verbose --timeout 900 + - name: ccache-clear + uses: ./.github/actions/ccache-clear + env: + GH_TOKEN: ${{ github.token }} + with: + key: webgpu-macos-latest + older: 5m + min: 1 + dry-run: ${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }} + ubuntu: runs-on: ubuntu-24.04 @@ -153,3 +163,13 @@ jobs: # This is using llvmpipe and runs slower than other backends # test-backend-ops is too slow on llvmpipe, skip it ctest -L main -E test-backend-ops --verbose --timeout 900 + + - name: ccache-clear + uses: ./.github/actions/ccache-clear + env: + GH_TOKEN: ${{ github.token }} + with: + key: webgpu-ubuntu-24.04 + older: 5m + min: 1 + dry-run: ${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }} diff --git a/.github/workflows/hip-quality-check.yml b/.github/workflows/hip-quality-check.yml index 5d23f01cf..ecc4615a1 100644 --- a/.github/workflows/hip-quality-check.yml +++ b/.github/workflows/hip-quality-check.yml @@ -84,3 +84,13 @@ jobs: cd build make -j $(nproc) 2>&1 | tee metrics.log | grep -v 'Rpass-analysis=kernel-resource-usage\|remark:\|^$' python3 ../scripts/hip/gcn-cdna-vgpr-check.py metrics.log + + - name: ccache-clear + uses: ./.github/actions/ccache-clear + env: + GH_TOKEN: ${{ github.token }} + with: + key: hip-quality-check-ubuntu-22.04 + older: 5m + min: 1 + dry-run: ${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }} diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml index 9fb4b4ba1..530ace7cd 100644 --- a/.github/workflows/server.yml +++ b/.github/workflows/server.yml @@ -128,6 +128,16 @@ jobs: export LLAMA_ARG_BACKEND_SAMPLING=1 SLOW_TESTS=1 ./tests.sh + - name: ccache-clear + uses: ./.github/actions/ccache-clear + env: + GH_TOKEN: ${{ github.token }} + with: + key: server-ubuntu-24.04-arm + older: 5m + min: 1 + dry-run: ${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }} + windows: runs-on: windows-2025 @@ -181,3 +191,13 @@ jobs: cd tools/server/tests export SLOW_TESTS="1" ./tests.sh + + - name: ccache-clear + uses: ./.github/actions/ccache-clear + env: + GH_TOKEN: ${{ github.token }} + with: + key: server-windows-2025-x64 + older: 5m + min: 1 + dry-run: ${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }} diff --git a/scripts/ccache-clear.sh b/scripts/ccache-clear.sh new file mode 100755 index 000000000..27fda3315 --- /dev/null +++ b/scripts/ccache-clear.sh @@ -0,0 +1,105 @@ +#!/bin/bash +# Delete GitHub Actions caches matching a key prefix, oldest first. +# +# Usage: ccache-clear.sh --key KEY [--older DURATION] [--min N] [--dry-run] +# --key: cache key prefix to match and delete (without the ccache- prefix) +# --older: only delete caches created more than DURATION ago (e.g. 5m, 1h, 1d); +# by default all matching caches are deleted +# --min: stop deleting if fewer than N caches would remain (default: 0) +# --dry-run: only print the caches that would be deleted, without deleting them +# +# Env (when running in GitHub Actions): +# GH_TOKEN: token for the gh CLI +# GITHUB_REPOSITORY: owner/repo of the caches to manage +set -euo pipefail + +KEY="" +OLDER="" +MIN=0 +DRY_RUN=false +while [[ $# -gt 0 ]]; do + case "$1" in + --key) [[ $# -ge 2 ]] || { echo "Missing value for $1" >&2; exit 1; }; KEY="$2"; shift 2 ;; + --older) [[ $# -ge 2 ]] || { echo "Missing value for $1" >&2; exit 1; }; OLDER="$2"; shift 2 ;; + --min) [[ $# -ge 2 ]] || { echo "Missing value for $1" >&2; exit 1; }; MIN="$2"; shift 2 ;; + --dry-run) DRY_RUN=true; shift ;; + *) echo "Unknown argument: $1"; exit 1 ;; + esac +done + +command -v gh >/dev/null 2>&1 || { echo "Error: GitHub CLI (gh) is required" >&2; exit 1; } +[[ -n "${GITHUB_REPOSITORY:-}" ]] || { echo "Error: GITHUB_REPOSITORY not set" >&2; exit 1; } +[[ -n "$KEY" ]] || { echo "Error: --key is required" >&2; exit 1; } +[[ "$MIN" =~ ^[0-9]+$ ]] || { echo "Invalid min value: $MIN" >&2; exit 1; } + +# Convert a duration (e.g. 90m, 1h, 1d, plain seconds) to seconds +to_seconds() { + local val="$1" + [[ "$val" =~ ^[0-9]+$ ]] && { echo "$val"; return 0; } + local num="${val%?}" unit="${val: -1}" mult + [[ "$num" =~ ^[0-9]+$ ]] || return 1 + case "$unit" in + s) mult=1 ;; + m) mult=60 ;; + h) mult=3600 ;; + d) mult=86400 ;; + *) return 1 ;; + esac + echo $((num * mult)) +} + +# Convert an ISO-8601 UTC timestamp (e.g. 2026-08-23T16:51:23.313693Z) to epoch seconds +to_epoch() { + local val="$1" out + # GNU date (e.g. Linux) + if out=$(date -d "$val" +%s 2>/dev/null) && [[ "$out" =~ ^[0-9]+$ ]]; then + echo "$out" + return 0 + fi + # BSD date (e.g. macOS); fractional seconds are not needed, TZ forces UTC + out=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%S" "${val:0:19}" +%s 2>/dev/null) || return 1 + [[ "$out" =~ ^[0-9]+$ ]] || return 1 + echo "$out" +} + +CACHES=$(gh cache list --repo "$GITHUB_REPOSITORY" --key "ccache-$KEY" --json id,key,createdAt --jq '.[] | [.createdAt, .id, .key] | @tsv' | LC_ALL=C sort) +if [[ -z "$CACHES" ]]; then + echo "No caches found with key prefix: $KEY" + exit 0 +fi + +TOTAL=$(( $(wc -l <<< "$CACHES") )) + +echo "Found $TOTAL cache(s) with key prefix: $KEY (oldest first):" +while IFS=$'\t' read -r CREATED ID CACHE_KEY; do + printf ' %s %s %s\n' "$CREATED" "$ID" "$CACHE_KEY" +done <<< "$CACHES" + +CUTOFF="" +if [[ -n "$OLDER" ]]; then + OLDER_SECONDS=$(to_seconds "$OLDER") || { echo "Invalid older value: $OLDER (expected e.g. 90m, 1h, 1d)" >&2; exit 1; } + CUTOFF=$(( $(date +%s) - OLDER_SECONDS )) +fi + +# Caches are sorted oldest first +DELETED=0 +while IFS=$'\t' read -r CREATED ID CACHE_KEY; do + if [[ -n "$CUTOFF" ]]; then + CREATED_SECONDS=$(to_epoch "$CREATED") || { echo "Failed to parse date: $CREATED" >&2; exit 1; } + if [[ "$CREATED_SECONDS" -ge "$CUTOFF" ]]; then + echo "Rest are not older than $OLDER, stopping" + break + fi + fi + if (( TOTAL - DELETED - 1 < MIN )); then + echo "Keeping at least $MIN cache(s), stopping" + break + fi + if [[ "$DRY_RUN" == "true" ]]; then + echo "Would delete cache: $ID ($CACHE_KEY)" + else + echo "Deleting cache: $ID ($CACHE_KEY)" + gh cache delete --repo "$GITHUB_REPOSITORY" "$ID" + fi + DELETED=$((DELETED + 1)) +done <<< "$CACHES" From 160c6b0bdd9739d6c8c8539e7067f64e8ea57067 Mon Sep 17 00:00:00 2001 From: Xuan-Son Nguyen Date: Mon, 24 Aug 2026 09:59:04 +0200 Subject: [PATCH 24/28] mtmd: video: fix moov atom at the end of file (#27596) * mtmd: video: fix moov at the end of file Co-authored-by: rkfg * fix SIGPIPE * windows: handle broken pipe case --------- Co-authored-by: rkfg --- tools/mtmd/mtmd-helper.cpp | 51 +++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/tools/mtmd/mtmd-helper.cpp b/tools/mtmd/mtmd-helper.cpp index f719323c0..f1defb647 100644 --- a/tools/mtmd/mtmd-helper.cpp +++ b/tools/mtmd/mtmd-helper.cpp @@ -42,6 +42,11 @@ #ifdef MTMD_VIDEO #include "sheredom/subprocess.h" #include +#ifndef _WIN32 +#include +#include +#include +#endif #endif // @@ -522,7 +527,8 @@ struct mtmd_helper_video { // RAII wrapper for managing subprocess struct subprocess_handle { struct subprocess_s proc = {}; - bool alive = false; + bool created = false; // process exists and must be cleaned up + bool alive = false; // process can still give us data std::thread feeder; subprocess_handle() = default; @@ -531,18 +537,27 @@ struct mtmd_helper_video { ~subprocess_handle() { stop(); } void stop() { - if (alive) { - subprocess_terminate(&proc); + // note: alive becomes false on stdout EOF, but the process still needs cleanup + if (!created) { + return; } + subprocess_terminate(&proc); +#ifdef _WIN32 + // no SIGPIPE on windows: a blocked feeder only gets a broken pipe once we close our read end of the child stdin + if (proc.hStdInput) { + CloseHandle(proc.hStdInput); + proc.hStdInput = nullptr; + } +#endif // join before destroy: feeder holds a FILE* from subprocess_stdin; // subprocess_destroy closes it, so the thread must finish first if (feeder.joinable()) { feeder.join(); } - if (alive) { - subprocess_destroy(&proc); - alive = false; - } + subprocess_join(&proc, nullptr); // reap the child, or else it stays a zombie + subprocess_destroy(&proc); + created = false; + alive = false; } FILE * stdout_pipe() { @@ -552,10 +567,21 @@ struct mtmd_helper_video { // buf is tied to lifetime of mtmd_helper_video, so it's guaranteed to outlive the feeder thread void start_feeder(const std::vector & buf) { feeder = std::thread([this, &buf]() { +#ifndef _WIN32 + // ffmpeg can exit before it reads all the input, for example when ffprobe already got the metadata. + // the write below must then fail with EPIPE, instead of killing the process with SIGPIPE + sigset_t sigpipe_set; + sigemptyset(&sigpipe_set); + sigaddset(&sigpipe_set, SIGPIPE); + pthread_sigmask(SIG_BLOCK, &sigpipe_set, nullptr); // linux sends the signal to the writing thread +#endif FILE * f = subprocess_stdin(&proc); if (!f) { return; } +#ifdef F_SETNOSIGPIPE + fcntl(fileno(f), F_SETNOSIGPIPE, 1); // macos/bsd send it to the process, so turn it off per fd +#endif fwrite(buf.data(), 1, buf.size(), f); fclose(f); proc.stdin_file = nullptr; // prevent double-close in subprocess_destroy @@ -601,7 +627,8 @@ struct mtmd_helper_video { LOG_ERR("%s: failed to launch ffprobe\n", __func__); return false; } - probe_sp.alive = true; + probe_sp.created = true; + probe_sp.alive = true; if (is_buf_input()) { probe_sp.start_feeder(input_buf); @@ -673,6 +700,11 @@ struct mtmd_helper_video { } cmd.push_back("-nostdin"); + if (is_buf_input()) { + // remove the 64KB read-ahead limit of cache:, or else ffmpeg cannot reach a moov atom at end of file + cmd.push_back("-read_ahead_limit"); + cmd.push_back("-1"); + } cmd.push_back("-i"); // cache:pipe:0 wraps stdin with a seekable in-memory cache, letting ffmpeg seek // backwards for container headers (e.g. MP4 moov atom at end of file) @@ -711,7 +743,8 @@ struct mtmd_helper_video { subprocess_option_search_user_path | subprocess_option_inherit_environment, &sp.proc); - sp.alive = (ret == 0); + sp.created = (ret == 0); + sp.alive = (ret == 0); LOG_DBG("%s: subprocess_create ret=%d proc_alive=%d\n", __func__, ret, (int)sp.alive); if (sp.alive && is_buf_input()) { From c1c766da59baa97ddc887e49b82cce718325c834 Mon Sep 17 00:00:00 2001 From: fairydreaming <166155368+fairydreaming@users.noreply.github.com> Date: Mon, 24 Aug 2026 10:07:12 +0200 Subject: [PATCH 25/28] webgpu : reorder includes since V that appears in common_decls.tmpl may be defined as K in flash_attn_decls.tmpl if KV_OVERLAP (#27545) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Stanisław Szymczyk --- ggml/src/ggml-webgpu/wgsl-shaders/flash_attn.wgsl | 3 +-- ggml/src/ggml-webgpu/wgsl-shaders/flash_attn_tile.wgsl | 2 +- ggml/src/ggml-webgpu/wgsl-shaders/flash_attn_vec_split.wgsl | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/ggml/src/ggml-webgpu/wgsl-shaders/flash_attn.wgsl b/ggml/src/ggml-webgpu/wgsl-shaders/flash_attn.wgsl index d5bf2af8d..a7dee6512 100644 --- a/ggml/src/ggml-webgpu/wgsl-shaders/flash_attn.wgsl +++ b/ggml/src/ggml-webgpu/wgsl-shaders/flash_attn.wgsl @@ -5,10 +5,9 @@ enable subgroups; enable chromium_experimental_subgroup_matrix; #define BYTE_HELPERS -#include "common_decls.tmpl" - #define FLASH_ATTN_SCALAR_KV #include "flash_attn_decls.tmpl" +#include "common_decls.tmpl" // Default values // The actual values are defined in shader-lib. diff --git a/ggml/src/ggml-webgpu/wgsl-shaders/flash_attn_tile.wgsl b/ggml/src/ggml-webgpu/wgsl-shaders/flash_attn_tile.wgsl index 8cd18b921..7edca84fc 100644 --- a/ggml/src/ggml-webgpu/wgsl-shaders/flash_attn_tile.wgsl +++ b/ggml/src/ggml-webgpu/wgsl-shaders/flash_attn_tile.wgsl @@ -2,8 +2,8 @@ enable f16; enable subgroups; #define BYTE_HELPERS -#include "common_decls.tmpl" #include "flash_attn_decls.tmpl" +#include "common_decls.tmpl" // Default values // The actual values are defined in shader-lib. diff --git a/ggml/src/ggml-webgpu/wgsl-shaders/flash_attn_vec_split.wgsl b/ggml/src/ggml-webgpu/wgsl-shaders/flash_attn_vec_split.wgsl index 42f3b1089..ae941245c 100644 --- a/ggml/src/ggml-webgpu/wgsl-shaders/flash_attn_vec_split.wgsl +++ b/ggml/src/ggml-webgpu/wgsl-shaders/flash_attn_vec_split.wgsl @@ -3,9 +3,9 @@ enable f16; enable subgroups; #define BYTE_HELPERS -#include "common_decls.tmpl" #define FLASH_ATTN_VEC_SPLIT #include "flash_attn_decls.tmpl" +#include "common_decls.tmpl" // Default values // The actual values are defined in shader-lib. From a14dba686aaafba3a2d6b5eb8820b0df5c5d2d92 Mon Sep 17 00:00:00 2001 From: Georgi Gerganov Date: Mon, 24 Aug 2026 12:35:08 +0300 Subject: [PATCH 26/28] ggml : shorten virtual device naming in CUDA and Metal (#27608) * ggml : shorten virtual device naming in CUDA and Metal Assisted-by: llama.cpp:DeepSeek-V4-Flash-0731 * ggml-metal : build device description at init Assisted-by: llama.cpp:DeepSeek-V4-Flash-0731 * cont : naming --- ggml/src/ggml-cuda/ggml-cuda.cu | 4 ++-- ggml/src/ggml-metal/ggml-metal-device.cpp | 4 ++-- ggml/src/ggml-metal/ggml-metal-device.h | 6 ++++-- ggml/src/ggml-metal/ggml-metal-device.m | 16 ++++++++++++++-- ggml/src/ggml-metal/ggml-metal.cpp | 2 +- 5 files changed, 23 insertions(+), 9 deletions(-) diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu index b4c128141..2456f7dcc 100644 --- a/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/ggml/src/ggml-cuda/ggml-cuda.cu @@ -4611,8 +4611,8 @@ static std::string ggml_cuda_device_description(int device) { const ggml_cuda_device_info & info = ggml_cuda_info(); std::string description = prop.name; if (info.device_count > info.physical_device_count) { - description += " (physical device " + std::to_string(info.devices[device].physical_device) + - ", virtual device " + std::to_string(info.devices[device].virtual_index) + ")"; + description += " (dev p" + std::to_string(info.devices[device].physical_device) + + "/v" + std::to_string(info.devices[device].virtual_index) + ")"; } return description; } diff --git a/ggml/src/ggml-metal/ggml-metal-device.cpp b/ggml/src/ggml-metal/ggml-metal-device.cpp index 52043696e..24caad7ff 100644 --- a/ggml/src/ggml-metal/ggml-metal-device.cpp +++ b/ggml/src/ggml-metal/ggml-metal-device.cpp @@ -17,10 +17,10 @@ struct ggml_metal_device_deleter { typedef std::unique_ptr ggml_metal_device_ptr; -ggml_metal_device_t ggml_metal_device_get(int device) { +ggml_metal_device_t ggml_metal_device_get(int device, int n_devices) { static std::vector devs; - devs.emplace_back(ggml_metal_device_init(device)); + devs.emplace_back(ggml_metal_device_init(device, n_devices)); return devs.back().get(); } diff --git a/ggml/src/ggml-metal/ggml-metal-device.h b/ggml/src/ggml-metal/ggml-metal-device.h index b7d466058..5f5410a03 100644 --- a/ggml/src/ggml-metal/ggml-metal-device.h +++ b/ggml/src/ggml-metal/ggml-metal-device.h @@ -259,6 +259,8 @@ enum ggml_metal_device_id { struct ggml_metal_device_props { int device; + int device_phys; + int device_virt; char name[128]; char desc[128]; @@ -286,10 +288,10 @@ typedef struct ggml_metal_event * ggml_metal_event_t; void ggml_metal_event_encode_signal(ggml_metal_event_t ev, ggml_metal_cmd_buf_t cmd_buf); void ggml_metal_event_encode_wait (ggml_metal_event_t ev, ggml_metal_cmd_buf_t cmd_buf); -ggml_metal_device_t ggml_metal_device_init(int device); +ggml_metal_device_t ggml_metal_device_init(int device, int n_devices); void ggml_metal_device_free(ggml_metal_device_t dev); -ggml_metal_device_t ggml_metal_device_get(int device); +ggml_metal_device_t ggml_metal_device_get(int device, int n_devices); void * ggml_metal_device_get_obj (ggml_metal_device_t dev); // id void * ggml_metal_device_get_queue(ggml_metal_device_t dev); // id diff --git a/ggml/src/ggml-metal/ggml-metal-device.m b/ggml/src/ggml-metal/ggml-metal-device.m index 312b00dc4..263016df7 100644 --- a/ggml/src/ggml-metal/ggml-metal-device.m +++ b/ggml/src/ggml-metal/ggml-metal-device.m @@ -711,7 +711,7 @@ static enum ggml_metal_device_id ggml_metal_device_id_parse(const char * name) { return GGML_METAL_DEVICE_GENERIC; } -ggml_metal_device_t ggml_metal_device_init(int device) { +ggml_metal_device_t ggml_metal_device_init(int device, int n_devices) { ggml_metal_device_t dev = calloc(1, sizeof(struct ggml_metal_device)); assert(dev != NULL); @@ -728,6 +728,12 @@ ggml_metal_device_t ggml_metal_device_init(int device) { dev->addr_virt = 0x000000400ULL; dev->props.device = device; + + // the Metal backend uses the system default device as the single physical device; + // additional (virtual) devices are emulated on top of it via GGML_METAL_DEVICES + dev->props.device_phys = 0; + dev->props.device_virt = device; + dev->props.has_simdgroup_reduction = [dev->mtl_device supportsFamily:MTLGPUFamilyApple7]; dev->props.has_simdgroup_reduction |= [dev->mtl_device supportsFamily:MTLGPUFamilyMetal3_GGML]; @@ -891,7 +897,13 @@ ggml_metal_device_t ggml_metal_device_init(int device) { } snprintf(dev->props.name, sizeof(dev->props.name), "%s%d", "MTL", device); - snprintf(dev->props.desc, sizeof(dev->props.desc), "%s", [[dev->mtl_device name] UTF8String]); + const char * gpu_name = [[dev->mtl_device name] UTF8String]; + if (n_devices > 1) { + snprintf(dev->props.desc, sizeof(dev->props.desc), "%s (dev p%d/v%d)", + gpu_name, dev->props.device_phys, dev->props.device_virt); + } else { + snprintf(dev->props.desc, sizeof(dev->props.desc), "%s", gpu_name); + } dev->library = ggml_metal_library_init(dev); if (!dev->library) { diff --git a/ggml/src/ggml-metal/ggml-metal.cpp b/ggml/src/ggml-metal/ggml-metal.cpp index 0e8d409e0..31aa61e32 100644 --- a/ggml/src/ggml-metal/ggml-metal.cpp +++ b/ggml/src/ggml-metal/ggml-metal.cpp @@ -891,7 +891,7 @@ static ggml_backend_dev_t ggml_backend_metal_device_init(ggml_backend_reg_t reg, return new ggml_backend_device { /* .iface = */ ggml_backend_metal_device_i, /* .reg = */ reg, - /* .context = */ ggml_metal_device_get(device), + /* .context = */ ggml_metal_device_get(device, g_devices), }; } From 71cc86fa41ff38c86e6409ba388dc7854e88588d Mon Sep 17 00:00:00 2001 From: jacekpoplawski <67507230+jacekpoplawski@users.noreply.github.com> Date: Mon, 24 Aug 2026 12:21:00 +0200 Subject: [PATCH 27/28] convert: fix GLM regression in index_tensors (#27655) --- conversion/glm.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/conversion/glm.py b/conversion/glm.py index 23fdbca88..7544f850c 100644 --- a/conversion/glm.py +++ b/conversion/glm.py @@ -122,7 +122,9 @@ class Glm4MoeModel(TextModel): self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count) def index_tensors(self, remote_hf_model_id: str | None = None): - type(self)._n_main_layers = self.hparams["num_hidden_layers"] + hparams = {**self.hparams, **self.hparams.get("text_config", {})} + key = next((k for k in ["n_layers", "num_hidden_layers", "n_layer", "num_layers"] if k in hparams), None) + type(self)._n_main_layers = hparams.get(key) return super().index_tensors(remote_hf_model_id=remote_hf_model_id) @classmethod From 7584430716ee229751771ed0d6bbcb780d105eeb Mon Sep 17 00:00:00 2001 From: fairydreaming <166155368+fairydreaming@users.noreply.github.com> Date: Mon, 24 Aug 2026 12:39:31 +0200 Subject: [PATCH 28/28] tests : disable DOTS3NOTE arch test for WebGPU (#27654) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Stanisław Szymczyk --- tests/test-llama-archs.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test-llama-archs.cpp b/tests/test-llama-archs.cpp index 83922c53b..b8fd66cca 100644 --- a/tests/test-llama-archs.cpp +++ b/tests/test-llama-archs.cpp @@ -507,7 +507,7 @@ static bool arch_supported(const llm_arch arch) { } // FIXME: these hit scheduler/view-backed-output issues with WebGPU on CI. #ifdef GGML_USE_WEBGPU - if (arch == LLM_ARCH_DEEPSEEK32 || arch == LLM_ARCH_GLM_DSA) { + if (arch == LLM_ARCH_DEEPSEEK32 || arch == LLM_ARCH_GLM_DSA || arch == LLM_ARCH_DOTS3NOTE) { return false; } #endif // GGML_USE_WEBGPU