mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-08-04 12:08:16 +02:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5788b510a1 | |||
| 2e17f69ef4 | |||
| 15831f579a | |||
| b5746d28ce | |||
| f26efa02a7 | |||
| cf06ad7dfe | |||
| b06fbc968b | |||
| 1269cb1ff1 | |||
| 935cad6497 | |||
| 22dc605c4e | |||
| 6c8dcaa7ae | |||
| 66fa168a56 | |||
| 0ef6e55edb | |||
| 94bc47f280 | |||
| fe2adf0e72 | |||
| 57c092139a | |||
| ee0445c99c | |||
| 99111b19ce | |||
| e8e06f78e2 |
@@ -119,6 +119,7 @@ jobs:
|
||||
run: |
|
||||
source ./vulkan_sdk/setup-env.sh
|
||||
cmake -B build \
|
||||
-DGGML_NATIVE=OFF \
|
||||
-DGGML_VULKAN=ON
|
||||
cmake --build build --config Release -j $(nproc)
|
||||
|
||||
|
||||
+2
-2
@@ -2582,7 +2582,7 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
|
||||
params.mtmd_batch_max_tokens = value;
|
||||
}
|
||||
).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_MTMD_BATCH_MAX_TOKENS"));
|
||||
if (llama_supports_rpc()) {
|
||||
if (params.is_gen_docs || llama_supports_rpc()) {
|
||||
add_opt(common_arg(
|
||||
{"--rpc"}, "SERVERS",
|
||||
"comma-separated list of RPC servers (host:port)",
|
||||
@@ -3331,7 +3331,7 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
|
||||
{"--tools"}, "TOOL1,TOOL2,...",
|
||||
"experimental: whether to enable built-in tools for AI agents - do not enable in untrusted environments (default: no tools)\n"
|
||||
"specify \"all\" to enable all tools\n"
|
||||
"available tools: read_file, file_glob_search, grep_search, exec_shell_command, write_file, edit_file, get_datetime\n"
|
||||
"available tools: read_file, file_glob_search, grep_search, exec_shell_command, write_file, edit_file, get_datetime, get_info\n"
|
||||
"note: for security reasons, this will limit --cors-origins to localhost by default",
|
||||
[](common_params & params, const std::string & value) {
|
||||
params.server_tools = parse_csv_row(value);
|
||||
|
||||
+26
-7
@@ -2114,6 +2114,11 @@ static common_chat_params common_chat_params_init_deepseek_v3_2(const common_cha
|
||||
auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE;
|
||||
auto include_grammar = has_response_format || (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE);
|
||||
|
||||
std::optional<json> additional_context;
|
||||
if (is_v4 && has_response_format) {
|
||||
additional_context = json{ { "response_format", inputs.json_schema } };
|
||||
}
|
||||
|
||||
const std::string DSML = "|DSML|";
|
||||
const std::string THINK_START = "<think>";
|
||||
const std::string THINK_END = "</think>";
|
||||
@@ -2125,9 +2130,12 @@ static common_chat_params common_chat_params_init_deepseek_v3_2(const common_cha
|
||||
const std::string PARAM_START = "<" + DSML + "parameter";
|
||||
const std::string PARAM_END = "</" + DSML + "parameter>";
|
||||
const std::string GEN_PROMPT = "<|Assistant|>";
|
||||
const std::string TC_SEPARATOR = "\n\n";
|
||||
|
||||
data.prompt = common_chat_template_direct_apply_impl(tmpl, inputs, adjusted_messages);
|
||||
data.generation_prompt = common_chat_template_generation_prompt_impl(tmpl, inputs, adjusted_messages);
|
||||
data.prompt = common_chat_template_direct_apply_impl(
|
||||
tmpl, inputs, adjusted_messages, std::nullopt, additional_context);
|
||||
data.generation_prompt = common_chat_template_generation_prompt_impl(
|
||||
tmpl, inputs, adjusted_messages, std::nullopt, additional_context);
|
||||
data.format = COMMON_CHAT_FORMAT_PEG_NATIVE;
|
||||
data.supports_thinking = true;
|
||||
data.thinking_start_tag = THINK_START;
|
||||
@@ -2141,9 +2149,16 @@ static common_chat_params common_chat_params_init_deepseek_v3_2(const common_cha
|
||||
if (inputs.has_continuation()) {
|
||||
const auto & msg = inputs.continue_msg;
|
||||
|
||||
data.generation_prompt = GEN_PROMPT + THINK_START + msg.reasoning_content;
|
||||
if (inputs.continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT) {
|
||||
data.generation_prompt += THINK_END + msg.render_content();
|
||||
if (is_v4 && msg.reasoning_content.empty()) {
|
||||
data.generation_prompt = GEN_PROMPT + THINK_END;
|
||||
if (inputs.continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT) {
|
||||
data.generation_prompt += msg.render_content();
|
||||
}
|
||||
} else {
|
||||
data.generation_prompt = GEN_PROMPT + THINK_START + msg.reasoning_content;
|
||||
if (inputs.continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT) {
|
||||
data.generation_prompt += THINK_END + msg.render_content();
|
||||
}
|
||||
}
|
||||
|
||||
data.prompt += data.generation_prompt;
|
||||
@@ -2242,7 +2257,9 @@ static common_chat_params common_chat_params_init_deepseek_v3_2(const common_cha
|
||||
|
||||
if (extract_reasoning && inputs.enable_thinking) {
|
||||
reasoning = p.optional(THINK_START + p.reasoning(p.until(THINK_END)) + THINK_END);
|
||||
reasoning_with_tc = THINK_START + p.reasoning(p.until_one_of({ FC_START, THINK_END })) + obligatory_tool_calls;
|
||||
reasoning_with_tc = THINK_START +
|
||||
p.reasoning(p.until_one_of({ TC_SEPARATOR + FC_START, FC_START, THINK_END })) +
|
||||
p.space() + obligatory_tool_calls;
|
||||
allow_reasoning_with_tc = true;
|
||||
} else if (extract_reasoning) {
|
||||
// Thinking disabled but reasoning extraction requested: the generation prompt
|
||||
@@ -2265,7 +2282,9 @@ static common_chat_params common_chat_params_init_deepseek_v3_2(const common_cha
|
||||
return generation_prompt + reasoning + p.content(p.rest()) + end;
|
||||
}
|
||||
|
||||
auto content_before_tools = p.negate(p.literal(THINK_START)) + p.content(p.until(FC_START));
|
||||
auto content_before_tools = p.negate(p.literal(THINK_START)) +
|
||||
p.content(p.until_one_of({ TC_SEPARATOR + FC_START, FC_START })) +
|
||||
p.space();
|
||||
return allow_reasoning_with_tc ? generation_prompt + (reasoning_with_tc | (reasoning + content_before_tools + tool_calls)) + end :
|
||||
generation_prompt + reasoning + content_before_tools + tool_calls + end;
|
||||
});
|
||||
|
||||
+28
-11
@@ -998,6 +998,23 @@ bool fs_is_directory(const std::string & path) {
|
||||
return std::filesystem::exists(dir) && std::filesystem::is_directory(dir);
|
||||
}
|
||||
|
||||
std::string common_get_env(const std::string & name) {
|
||||
const char * value = std::getenv(name.c_str());
|
||||
return value == nullptr ? "" : value;
|
||||
}
|
||||
|
||||
void common_set_env(const std::string & name, const std::string & value) {
|
||||
#if defined(_WIN32)
|
||||
_putenv_s(name.c_str(), value.c_str());
|
||||
#else
|
||||
if (value.empty()) {
|
||||
unsetenv(name.c_str());
|
||||
} else {
|
||||
setenv(name.c_str(), value.c_str(), 1);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string fs_get_cache_directory() {
|
||||
std::string cache_directory = "";
|
||||
auto ensure_trailing_slash = [](std::string p) {
|
||||
@@ -1463,18 +1480,18 @@ common_init_result_ptr common_init_from_params(common_params & params, bool mode
|
||||
common_init_result::~common_init_result() = default;
|
||||
|
||||
std::string common_get_model_endpoint() {
|
||||
const char * model_endpoint_env = getenv("MODEL_ENDPOINT");
|
||||
// We still respect the use of environment-variable "HF_ENDPOINT" for backward-compatibility.
|
||||
const char * hf_endpoint_env = getenv("HF_ENDPOINT");
|
||||
const char * endpoint_env = model_endpoint_env ? model_endpoint_env : hf_endpoint_env;
|
||||
std::string model_endpoint = "https://huggingface.co/";
|
||||
if (endpoint_env) {
|
||||
model_endpoint = endpoint_env;
|
||||
if (model_endpoint.back() != '/') {
|
||||
model_endpoint += '/';
|
||||
}
|
||||
std::string endpoint = common_get_env("MODEL_ENDPOINT");
|
||||
if (endpoint.empty()) {
|
||||
// the HF_ENDPOINT variable is respected for backward compatibility
|
||||
endpoint = common_get_env("HF_ENDPOINT");
|
||||
}
|
||||
return model_endpoint;
|
||||
if (endpoint.empty()) {
|
||||
return "https://huggingface.co/";
|
||||
}
|
||||
if (endpoint.back() != '/') {
|
||||
endpoint += '/';
|
||||
}
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
char * common_get_model_or_exit(int argc, char * argv[]) {
|
||||
|
||||
@@ -739,6 +739,8 @@ struct common_params {
|
||||
llama_progress_callback load_progress_callback = NULL;
|
||||
void * load_progress_callback_user_data = NULL;
|
||||
bool no_alloc = false; // Don't allocate model buffers
|
||||
|
||||
bool is_gen_docs = false; // whether we are running inside llama-gen-docs
|
||||
};
|
||||
|
||||
// call once at the start of a program if it uses libcommon
|
||||
@@ -863,6 +865,15 @@ std::string string_from(const struct llama_context * ctx, const struct llama_bat
|
||||
|
||||
bool glob_match(const std::string & pattern, const std::string & str);
|
||||
|
||||
//
|
||||
// Environment utils
|
||||
//
|
||||
|
||||
// portable environment access, an unset variable reads as an empty string
|
||||
// and setting an empty value unsets the variable
|
||||
std::string common_get_env(const std::string & name);
|
||||
void common_set_env(const std::string & name, const std::string & value);
|
||||
|
||||
//
|
||||
// Filesystem utils
|
||||
//
|
||||
|
||||
@@ -482,6 +482,7 @@ caps caps_get(jinja::program & prog) {
|
||||
});
|
||||
},
|
||||
[&](context & ctx) {
|
||||
ctx.set_val("enable_thinking", mk_val<value_bool>(true));
|
||||
caps_apply_preserve_reasoning(ctx, true);
|
||||
},
|
||||
nullptr, // tools_fn
|
||||
|
||||
+1
-1
@@ -383,7 +383,7 @@ struct common_sampler * common_sampler_init(
|
||||
samplers.push_back(llama_sampler_init_infill(vocab));
|
||||
break;
|
||||
case COMMON_SAMPLER_TYPE_PENALTIES:
|
||||
samplers.push_back(llama_sampler_init_penalties(params.penalty_last_n, params.penalty_repeat, params.penalty_freq, params.penalty_present));
|
||||
samplers.push_back(llama_sampler_init_penalties(llama_vocab_n_tokens(vocab), params.penalty_last_n, params.penalty_repeat, params.penalty_freq, params.penalty_present));
|
||||
break;
|
||||
case COMMON_SAMPLER_TYPE_ADAPTIVE_P:
|
||||
// the `adaptive-p` sampler is like `dist` and `mirostat` in that it selects
|
||||
|
||||
@@ -81,7 +81,7 @@ class ChatGLMModel(TextModel):
|
||||
|
||||
@staticmethod
|
||||
def token_bytes_to_string(b):
|
||||
from transformers.models.gpt2.tokenization_gpt2 import bytes_to_unicode # ty: ignore[unresolved-import]
|
||||
from transformers.convert_slow_tokenizer import bytes_to_unicode
|
||||
byte_encoder = bytes_to_unicode()
|
||||
return ''.join([byte_encoder[ord(char)] for char in b.decode('latin-1')])
|
||||
|
||||
|
||||
@@ -535,7 +535,10 @@ class DeepseekV4Model(TextModel):
|
||||
logger.info("Skipping %d DeepSeek-V4 MTP tensor(s) for conversion v0", type(self)._skipped_mtp_tensors)
|
||||
|
||||
# add a default chat template; if the model has a built-in template, it will be overridden later
|
||||
template_path = Path(__file__).parent.parent / "models" / "templates" / "deepseek-ai-DeepSeek-V4.jinja"
|
||||
model_id_hint = self.remote_hf_model_id or self.dir_model.name
|
||||
is_0731 = "0731" in model_id_hint
|
||||
template_name = "deepseek-ai-DeepSeek-V4-Flash-0731.jinja" if is_0731 else "deepseek-ai-DeepSeek-V4.jinja"
|
||||
template_path = Path(__file__).parent.parent / "models" / "templates" / template_name
|
||||
if template_path.is_file():
|
||||
with open(template_path, "r", encoding="utf-8") as f:
|
||||
self.gguf_writer.add_chat_template(f.read())
|
||||
|
||||
@@ -206,10 +206,70 @@ class Glm4MoeModel(TextModel):
|
||||
@ModelBase.register("Glm4MoeLiteForCausalLM")
|
||||
class Glm4MoeLiteModel(DeepseekV2Model):
|
||||
model_arch = gguf.MODEL_ARCH.DEEPSEEK2
|
||||
skip_mtp = False
|
||||
supports_mtp_export = True
|
||||
_n_main_layers: int | None = None
|
||||
|
||||
def set_vocab(self):
|
||||
return self._set_vocab_glm()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
num_hidden_layers = self.hparams["num_hidden_layers"]
|
||||
self.num_nextn_predict_layers = self.hparams.get("num_nextn_predict_layers", 0)
|
||||
self.skip_mtp = self.no_mtp or self.num_nextn_predict_layers == 0
|
||||
|
||||
if self.skip_mtp:
|
||||
self.block_count = num_hidden_layers
|
||||
else:
|
||||
self.block_count = num_hidden_layers + self.num_nextn_predict_layers
|
||||
|
||||
self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count)
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
|
||||
if self.skip_mtp:
|
||||
return
|
||||
|
||||
self.gguf_writer.add_nextn_predict_layers(self.num_nextn_predict_layers)
|
||||
|
||||
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):
|
||||
if (titem := super().filter_tensors(item)) is None:
|
||||
return None
|
||||
name, gen = titem
|
||||
|
||||
if cls._n_main_layers is not None:
|
||||
match = re.match(r"model\.layers\.(\d+)\.", name)
|
||||
is_mtp = match is not None and int(match.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 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"
|
||||
|
||||
|
||||
@ModelBase.register("GlmMoeDsaForCausalLM")
|
||||
class GlmMoeDsaModel(DeepseekV2Model):
|
||||
|
||||
+1
-1
@@ -119,7 +119,7 @@ class LlamaModel(TextModel):
|
||||
path_tekken_json = self.dir_model / "tekken.json"
|
||||
path_tokenizer_json = self.dir_model / "tokenizer.json"
|
||||
if path_tekken_json.is_file() and not path_tokenizer_json.is_file():
|
||||
self._set_vocab_mistral()
|
||||
return self._set_vocab_mistral()
|
||||
|
||||
tokenizer_config_file = self.dir_model / 'tokenizer_config.json'
|
||||
if tokenizer_config_file.is_file():
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ class QwenModel(TextModel):
|
||||
|
||||
@staticmethod
|
||||
def token_bytes_to_string(b):
|
||||
from transformers.models.gpt2.tokenization_gpt2 import bytes_to_unicode # ty: ignore[unresolved-import]
|
||||
from transformers.convert_slow_tokenizer import bytes_to_unicode
|
||||
byte_encoder = bytes_to_unicode()
|
||||
return ''.join([byte_encoder[ord(char)] for char in b.decode('latin-1')])
|
||||
|
||||
|
||||
+15
-15
@@ -23,16 +23,16 @@ Legend:
|
||||
| ARGMAX | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| ARGSORT | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ | 🟡 | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| CEIL | ❌ | ❌ | ✅ | 🟡 | 🟡 | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| CLAMP | ❌ | ✅ | ✅ | ✅ | 🟡 | ✅ | 🟡 | ✅ | 🟡 | ✅ | ❌ | ❌ |
|
||||
| COL2IM_1D | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| CLAMP | ❌ | ✅ | ✅ | ✅ | 🟡 | ✅ | 🟡 | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| COL2IM_1D | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| CONCAT | ❌ | ✅ | ✅ | 🟡 | 🟡 | ✅ | 🟡 | ✅ | ✅ | 🟡 | ❌ | ❌ |
|
||||
| CONT | ❌ | 🟡 | ✅ | ✅ | 🟡 | ✅ | 🟡 | ✅ | ✅ | 🟡 | ❌ | ❌ |
|
||||
| CONV_2D | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| CONV_2D | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | 🟡 | ✅ | ❌ | ❌ |
|
||||
| CONV_2D_DW | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| CONV_3D | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| CONV_3D | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| CONV_TRANSPOSE_1D | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| CONV_TRANSPOSE_2D | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| COS | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | 🟡 | ✅ | ❌ | ❌ |
|
||||
| COS | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| COUNT_EQUAL | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| CPY | ❌ | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | ❌ | ❌ |
|
||||
| CROSS_ENTROPY_LOSS | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
@@ -51,8 +51,8 @@ Legend:
|
||||
| FILL | ❌ | ❌ | ✅ | ✅ | 🟡 | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| FLASH_ATTN_EXT | ❌ | 🟡 | ✅ | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | ❌ | ❌ |
|
||||
| FLOOR | ❌ | ❌ | ✅ | 🟡 | 🟡 | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| GATED_DELTA_NET | ❌ | ❌ | ✅ | ❌ | ✅ | 🟡 | ❌ | ✅ | 🟡 | ✅ | ❌ | ❌ |
|
||||
| GATED_LINEAR_ATTN | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| GATED_DELTA_NET | ❌ | ❌ | ✅ | ❌ | ✅ | 🟡 | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| GATED_LINEAR_ATTN | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| GEGLU | ❌ | ✅ | ✅ | ✅ | 🟡 | 🟡 | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| GEGLU_ERF | ❌ | ✅ | ✅ | ✅ | 🟡 | 🟡 | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| GEGLU_QUICK | ❌ | ✅ | ✅ | ✅ | 🟡 | 🟡 | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
@@ -60,14 +60,14 @@ Legend:
|
||||
| GELU_ERF | ❌ | ✅ | ✅ | 🟡 | 🟡 | ✅ | 🟡 | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| GELU_QUICK | ❌ | ✅ | ✅ | 🟡 | 🟡 | ✅ | 🟡 | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| GET_ROWS | ❌ | 🟡 | ✅ | 🟡 | 🟡 | 🟡 | 🟡 | ✅ | ✅ | 🟡 | ❌ | ❌ |
|
||||
| GET_ROWS_BACK | ❌ | ❌ | 🟡 | 🟡 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| GET_ROWS_BACK | ❌ | ❌ | 🟡 | 🟡 | ❌ | ❌ | ❌ | ❌ | 🟡 | ❌ | ❌ | ❌ |
|
||||
| GROUP_NORM | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| HARDSIGMOID | ❌ | ✅ | ✅ | 🟡 | 🟡 | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| HARDSWISH | ❌ | ✅ | ✅ | 🟡 | 🟡 | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| IM2COL | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| IM2COL_3D | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| L2_NORM | ❌ | ✅ | ✅ | ✅ | 🟡 | ✅ | ❌ | ✅ | ✅ | 🟡 | ❌ | ❌ |
|
||||
| LEAKY_RELU | ❌ | ✅ | ✅ | ✅ | ❌ | 🟡 | ❌ | ✅ | 🟡 | ❌ | ❌ | ❌ |
|
||||
| LEAKY_RELU | ❌ | ✅ | ✅ | ✅ | ❌ | 🟡 | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| LIGHTNING_INDEXER | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| LOG | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| MEAN | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
@@ -76,13 +76,13 @@ Legend:
|
||||
| MUL_MAT_HADAMARD | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| MUL_MAT_ID | ❌ | 🟡 | ✅ | ✅ | 🟡 | 🟡 | 🟡 | ✅ | ✅ | 🟡 | 🟡 | ❌ |
|
||||
| NEG | ❌ | ✅ | ✅ | 🟡 | 🟡 | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| NORM | ❌ | ✅ | ✅ | ✅ | 🟡 | ✅ | ✅ | ✅ | 🟡 | 🟡 | ❌ | ❌ |
|
||||
| NORM | ❌ | ✅ | ✅ | ✅ | 🟡 | ✅ | ✅ | ✅ | ✅ | 🟡 | ❌ | ❌ |
|
||||
| OPT_STEP_ADAMW | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
| OPT_STEP_SGD | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
| OUT_PROD | 🟡 | 🟡 | 🟡 | 🟡 | ❌ | ❌ | ❌ | 🟡 | ❌ | ❌ | ❌ | 🟡 |
|
||||
| OUT_PROD | 🟡 | 🟡 | 🟡 | 🟡 | ❌ | ❌ | ❌ | 🟡 | 🟡 | ❌ | ❌ | 🟡 |
|
||||
| PAD | ❌ | 🟡 | ✅ | 🟡 | ❌ | 🟡 | 🟡 | 🟡 | ✅ | ✅ | ❌ | ❌ |
|
||||
| PAD_REFLECT_1D | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| POOL_1D | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| POOL_1D | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| POOL_2D | ❌ | 🟡 | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| REGLU | ❌ | ✅ | ✅ | ✅ | 🟡 | 🟡 | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| RELU | ❌ | ✅ | ✅ | 🟡 | 🟡 | ✅ | 🟡 | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
@@ -103,13 +103,13 @@ Legend:
|
||||
| SIGMOID | ❌ | ✅ | ✅ | 🟡 | 🟡 | ✅ | 🟡 | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| SILU | ❌ | ✅ | ✅ | 🟡 | 🟡 | ✅ | 🟡 | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| SILU_BACK | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
| SIN | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | 🟡 | ✅ | ❌ | ❌ |
|
||||
| SIN | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| SOFTPLUS | ❌ | ❌ | ✅ | 🟡 | 🟡 | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| SOFT_MAX | ❌ | 🟡 | ✅ | ✅ | 🟡 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| SOFT_MAX_BACK | ❌ | ❌ | 🟡 | 🟡 | ❌ | ❌ | ❌ | 🟡 | ✅ | ❌ | ❌ | ❌ |
|
||||
| SOLVE_TRI | ❌ | ❌ | ✅ | 🟡 | 🟡 | ✅ | ❌ | 🟡 | ✅ | ✅ | ❌ | ❌ |
|
||||
| SQR | ❌ | ✅ | ✅ | ✅ | 🟡 | ✅ | ✅ | ✅ | 🟡 | ✅ | ❌ | ❌ |
|
||||
| SQRT | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | 🟡 | ✅ | ❌ | ❌ |
|
||||
| SQR | ❌ | ✅ | ✅ | ✅ | 🟡 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| SQRT | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| SSM_CONV | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| SSM_SCAN | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | 🟡 | 🟡 | ✅ | ❌ | ❌ |
|
||||
| STEP | ❌ | ✅ | ✅ | 🟡 | 🟡 | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
|
||||
+3989
-1112
File diff suppressed because it is too large
Load Diff
@@ -70,6 +70,8 @@ static void write_table(std::ostringstream & ss, std::vector<common_arg *> & opt
|
||||
|
||||
static void write_help(std::ostringstream & ss, const md_file & md) {
|
||||
common_params params;
|
||||
params.is_gen_docs = true;
|
||||
|
||||
auto ctx_arg = common_params_parser_init(params, md.ex);
|
||||
|
||||
std::vector<common_arg *> common_options;
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ project("ggml" C CXX ASM)
|
||||
### GGML Version
|
||||
set(GGML_VERSION_MAJOR 0)
|
||||
set(GGML_VERSION_MINOR 18)
|
||||
set(GGML_VERSION_PATCH 0)
|
||||
set(GGML_VERSION_PATCH 1)
|
||||
set(GGML_VERSION_BASE "${GGML_VERSION_MAJOR}.${GGML_VERSION_MINOR}.${GGML_VERSION_PATCH}")
|
||||
|
||||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/")
|
||||
|
||||
@@ -127,7 +127,15 @@ static void concat_T_sycl_non_cont(
|
||||
int64_t ne2, int64_t ne3, uint64_t nb0, uint64_t nb1, uint64_t nb2,
|
||||
uint64_t nb3, int32_t dim) {
|
||||
sycl::range<3> gridDim(ne3, ne2, ne1);
|
||||
stream->parallel_for(sycl::nd_range<3>(gridDim, sycl::range<3>(1, 1, 1)), [=](sycl::nd_item<3> item_ct1) {
|
||||
|
||||
// Avoid oversubscribing device when there is not enough elements along the innermost dim to
|
||||
// fill a full SYCL_CONCAT_BLOCK_SIZE. For larger # of elements, the full SYCL_CONCAT_BLOCK_SIZE
|
||||
// is used.
|
||||
const int64_t ne0_pad = GGML_PAD(ne0, WARP_SIZE);
|
||||
const int64_t block_ne0 = ne0_pad < SYCL_CONCAT_BLOCK_SIZE ? ne0_pad : (int64_t) SYCL_CONCAT_BLOCK_SIZE;
|
||||
sycl::range<3> blockDim(1, 1, block_ne0);
|
||||
|
||||
stream->parallel_for(sycl::nd_range<3>(gridDim * blockDim, blockDim), [=](sycl::nd_item<3> item_ct1) {
|
||||
int64_t i3 = item_ct1.get_group(0);
|
||||
int64_t i2 = item_ct1.get_group(1);
|
||||
int64_t i1 = item_ct1.get_group(2);
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "fattn-onednn.hpp"
|
||||
#include "fattn-tile.hpp"
|
||||
#include "convert.hpp"
|
||||
|
||||
// set minimum query length to treat as prefill (32)
|
||||
#define GGML_SYCL_FA_ONEDNN_MIN_Q 32
|
||||
@@ -33,10 +35,30 @@ bool ggml_sycl_flash_attn_ext_onednn_supported(const ggml_tensor * dst) {
|
||||
const ggml_tensor * mask = dst->src[3];
|
||||
const ggml_tensor * sinks = dst->src[4];
|
||||
|
||||
// gate for f16 KV only for now
|
||||
// need to implement quantized KV
|
||||
// F16 KV: native SDPA at any KV length.
|
||||
// Non-F16: dequant to F16 then SDPA at prefill lengths. Only the
|
||||
// standard quantized KV cache types (Q4_0-Q8_0) and F32 are accepted
|
||||
// because their to_fp16_sycl conversion is verified. BF16 and IQ*
|
||||
// are excluded: BF16 needs a strided conversion kernel that does not
|
||||
// exist yet; IQ types are model-weight-only quants with no dequant
|
||||
// registration and are never used as KV caches.
|
||||
if (K->type != GGML_TYPE_F16 || V->type != GGML_TYPE_F16) {
|
||||
return false;
|
||||
auto kt = K->type, vt = V->type;
|
||||
bool k_ok = kt == GGML_TYPE_F32 || kt == GGML_TYPE_Q4_0 || kt == GGML_TYPE_Q4_1 ||
|
||||
kt == GGML_TYPE_Q5_0 || kt == GGML_TYPE_Q5_1 || kt == GGML_TYPE_Q8_0;
|
||||
bool v_ok = vt == GGML_TYPE_F32 || vt == GGML_TYPE_Q4_0 || vt == GGML_TYPE_Q4_1 ||
|
||||
vt == GGML_TYPE_Q5_0 || vt == GGML_TYPE_Q5_1 || vt == GGML_TYPE_Q8_0;
|
||||
if (!k_ok || !v_ok) {
|
||||
return false;
|
||||
}
|
||||
if (Q->ne[1] < 32 || K->ne[1] < 1024) {
|
||||
return false;
|
||||
}
|
||||
for (const ggml_tensor * t : {K, V}) {
|
||||
if (t->type == GGML_TYPE_F16 && t->nb[1] % (t->ne[0] * 2) != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Optional KV-length ceiling (GGML_SYCL_FA_ONEDNN_MAX_KV, 0 = unlimited). Escape hatch:
|
||||
// very long sequences make the fused SDPA slow enough to risk the xe driver watchdog on
|
||||
@@ -205,13 +227,101 @@ void ggml_sycl_flash_attn_ext_onednn(ggml_backend_sycl_context & ctx, ggml_tenso
|
||||
dnnl::engine eng = ctx.engine_dnnl(stream);
|
||||
dnnl::stream strm = ctx.stream_dnnl(stream);
|
||||
|
||||
// cont/cast inputs to contiguous f16 (head-major) -- the layout the fast systolic path wants.
|
||||
ggml_sycl_pool_alloc<sycl::half> Qf(ctx.pool(), (size_t) H * q * d);
|
||||
ggml_sycl_pool_alloc<sycl::half> Kf(ctx.pool(), (size_t) Hkv * seq * d);
|
||||
ggml_sycl_pool_alloc<sycl::half> Vf(ctx.pool(), (size_t) Hkv * seq * d);
|
||||
cont_to_f16_sycl<float> ((const char *) Q->data, Qf.get(), d, q, H, mb, Q->nb[1], Q->nb[2], Q->nb[3], stream);
|
||||
cont_to_f16_sycl<sycl::half>((const char *) K->data, Kf.get(), d, seq, Hkv, mb, K->nb[1], K->nb[2], K->nb[3], stream);
|
||||
cont_to_f16_sycl<sycl::half>((const char *) V->data, Vf.get(), d, seq, Hkv, mb, V->nb[1], V->nb[2], V->nb[3], stream);
|
||||
// Q: always f32 -- copy to dense f16.
|
||||
ggml_sycl_pool_alloc<sycl::half> Qf(ctx.pool(), (size_t) H * q * d);
|
||||
cont_to_f16_sycl<float>((const char *) Q->data, Qf.get(), d, q, H, mb, Q->nb[1], Q->nb[2], Q->nb[3], stream);
|
||||
|
||||
// K/V: use pool-alloc for both F16 and dequant paths.
|
||||
sycl::half * K_ptr = nullptr;
|
||||
sycl::half * V_ptr = nullptr;
|
||||
std::optional<ggml_sycl_pool_alloc<sycl::half>> Kf_pool;
|
||||
std::optional<ggml_sycl_pool_alloc<sycl::half>> Vf_pool;
|
||||
|
||||
if (K->type == GGML_TYPE_F16 && V->type == GGML_TYPE_F16) {
|
||||
Kf_pool.emplace(ctx.pool(), (size_t) Hkv * seq * d);
|
||||
Vf_pool.emplace(ctx.pool(), (size_t) Hkv * seq * d);
|
||||
cont_to_f16_sycl<sycl::half>((const char *) K->data, Kf_pool->get(), d, seq, Hkv, mb, K->nb[1], K->nb[2], K->nb[3], stream);
|
||||
cont_to_f16_sycl<sycl::half>((const char *) V->data, Vf_pool->get(), d, seq, Hkv, mb, V->nb[1], V->nb[2], V->nb[3], stream);
|
||||
K_ptr = Kf_pool->get();
|
||||
V_ptr = Vf_pool->get();
|
||||
} else if (ggml_is_quantized(K->type)) {
|
||||
// Quantized K/V: dequant to dense F16 using pool, same lifetime as F16 path.
|
||||
Kf_pool.emplace(ctx.pool(), ggml_nelements(K));
|
||||
K_ptr = Kf_pool->get();
|
||||
{
|
||||
const char * K_data = (const char *)K->data;
|
||||
const bool k_non_dense = ((int64_t)K->ne[1] * K->nb[1] != K->nb[2]) && K->ne[2] > 1;
|
||||
const bool k_gemma = k_non_dense &&
|
||||
((int64_t)K->nb[2] < (int64_t)K->ne[1] * (int64_t)K->nb[1]);
|
||||
if (ggml_is_contiguously_allocated(K) && !k_non_dense) {
|
||||
to_fp16_sycl_t to_fp16 = ggml_get_to_fp16_sycl(K->type, dst);
|
||||
to_fp16(K_data, K_ptr, ggml_nelements(K), stream);
|
||||
} else {
|
||||
const size_t bs = ggml_blck_size(K->type);
|
||||
const size_t ts = ggml_type_size(K->type);
|
||||
to_fp16_nc_sycl_t to_fp16 = ggml_get_to_fp16_nc_sycl(K->type);
|
||||
int64_t s01, s02, s03;
|
||||
if (k_gemma) {
|
||||
const int64_t blk_per_row = (int64_t)K->ne[0] / bs;
|
||||
s01 = (int64_t)Hkv * blk_per_row;
|
||||
s02 = blk_per_row;
|
||||
s03 = (int64_t)K->ne[1] * s01;
|
||||
} else {
|
||||
s01 = (int64_t)K->nb[1] / ts;
|
||||
s02 = (int64_t)K->nb[2] / ts;
|
||||
s03 = (int64_t)K->nb[3] / ts;
|
||||
}
|
||||
to_fp16(K_data, K_ptr,
|
||||
K->ne[0], K->ne[1], K->ne[2], K->ne[3],
|
||||
s01, s02, s03, stream);
|
||||
}
|
||||
}
|
||||
// Quantized V: always dequant separately. Even when K and V share
|
||||
// the same underlying allocation (V is a view of K with the same
|
||||
// data pointer), their logical values differ because the quantized
|
||||
// elements at different positions/offsets represent different K/V
|
||||
// data. Master's F16 path also never aliases K and V.
|
||||
Vf_pool.emplace(ctx.pool(), ggml_nelements(V));
|
||||
V_ptr = Vf_pool->get();
|
||||
{
|
||||
const char * V_data = (const char *)V->data;
|
||||
const bool v_non_dense = ((int64_t)V->ne[1] * V->nb[1] != V->nb[2]) && V->ne[2] > 1;
|
||||
const bool v_gemma = v_non_dense &&
|
||||
((int64_t)V->nb[2] < (int64_t)V->ne[1] * (int64_t)V->nb[1]);
|
||||
if (ggml_is_contiguously_allocated(V) && !v_non_dense) {
|
||||
to_fp16_sycl_t to_fp16 = ggml_get_to_fp16_sycl(V->type, dst);
|
||||
to_fp16(V_data, V_ptr, ggml_nelements(V), stream);
|
||||
} else {
|
||||
const size_t bs = ggml_blck_size(V->type);
|
||||
const size_t ts = ggml_type_size(V->type);
|
||||
to_fp16_nc_sycl_t to_fp16 = ggml_get_to_fp16_nc_sycl(V->type);
|
||||
int64_t s01, s02, s03;
|
||||
if (v_gemma) {
|
||||
const int64_t blk_per_row = (int64_t)V->ne[0] / bs;
|
||||
s01 = (int64_t)V->ne[2] * blk_per_row;
|
||||
s02 = blk_per_row;
|
||||
s03 = (int64_t)V->ne[1] * s01;
|
||||
} else {
|
||||
s01 = (int64_t)V->nb[1] / ts;
|
||||
s02 = (int64_t)V->nb[2] / ts;
|
||||
s03 = (int64_t)V->nb[3] / ts;
|
||||
}
|
||||
to_fp16(V_data, V_ptr,
|
||||
V->ne[0], V->ne[1], V->ne[2], V->ne[3],
|
||||
s01, s02, s03, stream);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// F32: strided copy to dense F16 via cont_to_f16_sycl<float>.
|
||||
Kf_pool.emplace(ctx.pool(), ggml_nelements(K));
|
||||
K_ptr = Kf_pool->get();
|
||||
cont_to_f16_sycl<float>((const char *) K->data, K_ptr, K->ne[0], K->ne[1], K->ne[2], K->ne[3],
|
||||
K->nb[1], K->nb[2], K->nb[3], stream);
|
||||
Vf_pool.emplace(ctx.pool(), ggml_nelements(V));
|
||||
V_ptr = Vf_pool->get();
|
||||
cont_to_f16_sycl<float>((const char *) V->data, V_ptr, V->ne[0], V->ne[1], V->ne[2], V->ne[3],
|
||||
V->nb[1], V->nb[2], V->nb[3], stream);
|
||||
}
|
||||
|
||||
// divide-by-(1/scale) reproduces ggml's score *= kq_scale on the proven probe graph.
|
||||
//
|
||||
@@ -244,8 +354,8 @@ void ggml_sycl_flash_attn_ext_onednn(ggml_backend_sycl_context & ctx, ggml_tenso
|
||||
|
||||
auto id2ptr = [&](size_t r) -> void * {
|
||||
if (r == E.id_q) return Qf.get();
|
||||
if (r == E.id_k) return Kf.get();
|
||||
if (r == E.id_v) return Vf.get();
|
||||
if (r == E.id_k) return K_ptr;
|
||||
if (r == E.id_v) return V_ptr;
|
||||
if (r == E.id_scale) return scale_dev;
|
||||
if (r == E.id_mask) return (void *) mask->data;
|
||||
return nullptr;
|
||||
|
||||
@@ -97,7 +97,7 @@ static void ggml_sycl_flash_attn_ext_vec(ggml_backend_sycl_context & ctx, ggml_t
|
||||
enum best_fattn_kernel {
|
||||
BEST_FATTN_KERNEL_NONE = 0,
|
||||
BEST_FATTN_KERNEL_VEC = 100,
|
||||
BEST_FATTN_KERNEL_ONEDNN = 150, // added enum for onednn==150
|
||||
BEST_FATTN_KERNEL_ONEDNN = 150, // oneDNN SDPA: native F16 (PR #25222)
|
||||
BEST_FATTN_KERNEL_TILE = 200,
|
||||
BEST_FATTN_KERNEL_MKL = 300,
|
||||
};
|
||||
@@ -130,6 +130,14 @@ static best_fattn_kernel ggml_sycl_get_best_fattn_kernel(const int device, const
|
||||
|
||||
bool gqa_opt_applies = gqa_ratio >= 2 && mask && max_bias == 0.0f && K->ne[1] % FATTN_KQ_STRIDE == 0;
|
||||
|
||||
// XMX-accelerated path: oneDNN SDPA (native F16 and dequant+non-F16).
|
||||
// ONEDNN requires min 32 query tokens — short-circuit decode to avoid
|
||||
// calling _supported() on every decode FA call.
|
||||
if (Q->ne[1] >= 32
|
||||
&& ggml_sycl_flash_attn_ext_onednn_supported(dst)) {
|
||||
return BEST_FATTN_KERNEL_ONEDNN;
|
||||
}
|
||||
|
||||
// MKL path: XMX-accelerated GEMM for prompt processing (all KV cache types).
|
||||
// The MKL kernel converts non-F16 K/V to F16 via to_fp16_sycl before GEMM,
|
||||
// so quantized, F16, BF16, and F32 caches all benefit from XMX acceleration.
|
||||
@@ -167,7 +175,6 @@ static best_fattn_kernel ggml_sycl_get_best_fattn_kernel(const int device, const
|
||||
return BEST_FATTN_KERNEL_MKL;
|
||||
}
|
||||
}
|
||||
|
||||
for (const ggml_tensor * t : {Q, K, V, mask}) {
|
||||
if (t == nullptr || ggml_is_quantized(t->type)) {
|
||||
continue;
|
||||
@@ -215,6 +222,7 @@ static best_fattn_kernel ggml_sycl_get_best_fattn_kernel(const int device, const
|
||||
switch (K->type) {
|
||||
case GGML_TYPE_F32:
|
||||
case GGML_TYPE_F16:
|
||||
case GGML_TYPE_BF16:
|
||||
break;
|
||||
case GGML_TYPE_Q4_1:
|
||||
case GGML_TYPE_Q5_0:
|
||||
@@ -233,8 +241,11 @@ static best_fattn_kernel ggml_sycl_get_best_fattn_kernel(const int device, const
|
||||
return BEST_FATTN_KERNEL_NONE;
|
||||
}
|
||||
|
||||
// For small batch sizes the vector kernel may be preferable over the kernels optimized for large batch sizes:
|
||||
const bool can_use_vector_kernel = Q->ne[0] <= 512 && Q->ne[0] % 64 == 0 && K->ne[1] % FATTN_KQ_STRIDE == 0;
|
||||
// For small batch sizes the vector kernel may be preferable over the kernels optimized for large batch sizes.
|
||||
// BF16 is excluded: the VEC kernel has no BF16 template (it needs GGML_SYCL_FA_ALL_QUANTS for non-F16/Q4_0/Q8_0).
|
||||
const bool has_bf16 = (K->type == GGML_TYPE_BF16 || V->type == GGML_TYPE_BF16);
|
||||
const bool can_use_vector_kernel = Q->ne[0] <= 512 && Q->ne[0] % 64 == 0 && K->ne[1] % FATTN_KQ_STRIDE == 0
|
||||
&& !has_bf16;
|
||||
|
||||
// Fused-XMX path: oneDNN Graph SDPA (flash attention). Strictly
|
||||
// additive -- taken only when statically supported, otherwise falls through to VEC/TILE below.
|
||||
@@ -276,6 +287,7 @@ void ggml_sycl_flash_attn_ext(ggml_backend_sycl_context & ctx, ggml_tensor * dst
|
||||
const char * kname = "TILE";
|
||||
best_fattn_kernel k = ggml_sycl_get_best_fattn_kernel(ctx.device, dst);
|
||||
if (k == BEST_FATTN_KERNEL_MKL) kname = "MKL";
|
||||
if (k == BEST_FATTN_KERNEL_ONEDNN) kname = "ONEDNN";
|
||||
if (k == BEST_FATTN_KERNEL_VEC) kname = "VEC";
|
||||
int64_t delta = 0;
|
||||
if (Dk == 256) {
|
||||
@@ -292,7 +304,8 @@ void ggml_sycl_flash_attn_ext(ggml_backend_sycl_context & ctx, ggml_tensor * dst
|
||||
(long long)V_dbg->ne[1]);
|
||||
}
|
||||
|
||||
switch (ggml_sycl_get_best_fattn_kernel(ggml_sycl_get_device(), dst)) {
|
||||
const best_fattn_kernel fk = ggml_sycl_get_best_fattn_kernel(ggml_sycl_get_device(), dst);
|
||||
switch (fk) {
|
||||
case BEST_FATTN_KERNEL_NONE:
|
||||
GGML_ABORT("Not support Flash-Attention");
|
||||
case BEST_FATTN_KERNEL_ONEDNN:
|
||||
@@ -331,6 +344,7 @@ void ggml_sycl_flash_attn_ext(ggml_backend_sycl_context & ctx, ggml_tensor * dst
|
||||
q->wait();
|
||||
const char * kname = "???";
|
||||
best_fattn_kernel kb = ggml_sycl_get_best_fattn_kernel(ctx.device, dst);
|
||||
if (kb == BEST_FATTN_KERNEL_ONEDNN) kname = "ONEDNN";
|
||||
if (kb == BEST_FATTN_KERNEL_MKL) kname = "MKL";
|
||||
if (kb == BEST_FATTN_KERNEL_TILE) kname = "TILE";
|
||||
if (kb == BEST_FATTN_KERNEL_VEC) kname = "VEC";
|
||||
@@ -354,6 +368,7 @@ void ggml_sycl_flash_attn_ext(ggml_backend_sycl_context & ctx, ggml_tensor * dst
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool ggml_sycl_flash_attn_ext_supported(int device, const ggml_tensor * dst) {
|
||||
|
||||
@@ -1026,6 +1026,7 @@ struct vk_device_struct {
|
||||
vk_pipeline pipeline_pool2d_f32;
|
||||
vk_pipeline pipeline_rwkv_wkv6_f32;
|
||||
vk_pipeline pipeline_rwkv_wkv7_f32;
|
||||
vk_pipeline pipeline_gated_linear_attn_f32;
|
||||
// [size_idx][kda] where size_idx: 0=d16, 1=d32, 2=d64, 3=d128
|
||||
vk_pipeline pipeline_gated_delta_net[4][2];
|
||||
vk_pipeline pipeline_ssm_scan_f32_d128;
|
||||
@@ -1747,6 +1748,13 @@ struct vk_op_rwkv_wkv7_push_constants {
|
||||
uint32_t C;
|
||||
uint32_t H;
|
||||
};
|
||||
struct vk_op_gated_linear_attn_push_constants {
|
||||
uint32_t B;
|
||||
uint32_t T;
|
||||
uint32_t C;
|
||||
uint32_t H;
|
||||
float scale;
|
||||
};
|
||||
struct vk_op_gated_delta_net_push_constants {
|
||||
uint32_t H;
|
||||
uint32_t n_tokens;
|
||||
@@ -5665,6 +5673,8 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) {
|
||||
|
||||
ggml_vk_create_pipeline(device, device->pipeline_rwkv_wkv7_f32, "rwkv_wkv7_f32", rwkv_wkv7_f32_len, rwkv_wkv7_f32_data, "main", 8, sizeof(vk_op_rwkv_wkv7_push_constants), {1, 1, 1}, {device->subgroup_size}, 1);
|
||||
|
||||
ggml_vk_create_pipeline(device, device->pipeline_gated_linear_attn_f32, "gated_linear_attn_f32", gated_linear_attn_f32_len, gated_linear_attn_f32_data, "main", 6, sizeof(vk_op_gated_linear_attn_push_constants), {1, 1, 1}, {}, 1);
|
||||
|
||||
{
|
||||
const uint32_t gdn_sizes[] = {16, 32, 64, 128};
|
||||
const char * gdn_names[][2] = {
|
||||
@@ -11392,6 +11402,11 @@ static vk_pipeline ggml_vk_op_get_pipeline(ggml_backend_vk_context * ctx, const
|
||||
return ctx->device->pipeline_rwkv_wkv7_f32;
|
||||
}
|
||||
return nullptr;
|
||||
case GGML_OP_GATED_LINEAR_ATTN:
|
||||
if (src0->type == GGML_TYPE_F32 && dst->type == GGML_TYPE_F32) {
|
||||
return ctx->device->pipeline_gated_linear_attn_f32;
|
||||
}
|
||||
return nullptr;
|
||||
case GGML_OP_GATED_DELTA_NET:
|
||||
if (src0->type == GGML_TYPE_F32 && dst->type == GGML_TYPE_F32) {
|
||||
const uint32_t S_v = dst->src[2]->ne[0];
|
||||
@@ -12422,6 +12437,41 @@ static void ggml_vk_rwkv_wkv7(ggml_backend_vk_context * ctx, vk_context& subctx,
|
||||
);
|
||||
}
|
||||
|
||||
static void ggml_vk_gated_linear_attn(ggml_backend_vk_context * ctx, vk_context& subctx, ggml_tensor * dst) {
|
||||
const size_t seq_length = dst->src[0]->ne[2];
|
||||
const size_t n_embed = dst->ne[0];
|
||||
const size_t n_heads = dst->src[0]->ne[1];
|
||||
const size_t n_seqs = dst->src[4]->ne[1];
|
||||
|
||||
float scale;
|
||||
memcpy(&scale, dst->op_params, sizeof(float));
|
||||
|
||||
GGML_ASSERT(dst->buffer != nullptr);
|
||||
|
||||
vk_pipeline pipeline = ggml_vk_op_get_pipeline(ctx, dst->src[0], dst->src[1], dst->src[2], dst, dst->op);
|
||||
GGML_ASSERT(pipeline != nullptr);
|
||||
|
||||
ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1);
|
||||
|
||||
vk_subbuffer dst_buf = ggml_vk_tensor_subbuffer(ctx, dst);
|
||||
vk_subbuffer src_buf[5] = {};
|
||||
for (int i = 0; i < 5; i++) {
|
||||
src_buf[i] = ggml_vk_tensor_subbuffer(ctx, dst->src[i]);
|
||||
}
|
||||
|
||||
const vk_op_gated_linear_attn_push_constants pc = {
|
||||
(uint32_t)n_seqs,
|
||||
(uint32_t)seq_length,
|
||||
(uint32_t)n_embed,
|
||||
(uint32_t)n_heads,
|
||||
scale,
|
||||
};
|
||||
|
||||
ggml_vk_dispatch_pipeline(ctx, subctx, pipeline,
|
||||
{src_buf[0], src_buf[1], src_buf[2], src_buf[3], src_buf[4], dst_buf},
|
||||
pc, { (uint32_t)(n_seqs * n_heads), 1, 1 });
|
||||
}
|
||||
|
||||
static void ggml_vk_gated_delta_net(ggml_backend_vk_context * ctx, vk_context& subctx, ggml_tensor * dst) {
|
||||
const ggml_tensor * src_q = dst->src[0];
|
||||
const ggml_tensor * src_v = dst->src[2];
|
||||
@@ -15421,6 +15471,11 @@ static bool ggml_vk_build_graph(ggml_backend_vk_context * ctx, ggml_cgraph * cgr
|
||||
|
||||
break;
|
||||
|
||||
case GGML_OP_GATED_LINEAR_ATTN:
|
||||
ggml_vk_gated_linear_attn(ctx, compute_ctx, node);
|
||||
|
||||
break;
|
||||
|
||||
case GGML_OP_GATED_DELTA_NET:
|
||||
ggml_vk_gated_delta_net(ctx, compute_ctx, node);
|
||||
|
||||
@@ -18128,6 +18183,9 @@ static bool ggml_backend_vk_device_supports_op(ggml_backend_dev_t dev, const ggm
|
||||
case GGML_OP_RWKV_WKV6:
|
||||
case GGML_OP_RWKV_WKV7:
|
||||
return true; // all inputs are contiguous, see ggml.c
|
||||
case GGML_OP_GATED_LINEAR_ATTN:
|
||||
// the shader block size is hardcoded to head_size 64
|
||||
return op->src[0]->type == GGML_TYPE_F32 && op->type == GGML_TYPE_F32 && op->src[0]->ne[0] == 64;
|
||||
case GGML_OP_GATED_DELTA_NET:
|
||||
{
|
||||
const uint32_t S_v = op->src[2]->ne[0];
|
||||
@@ -19117,6 +19175,10 @@ static void ggml_vk_check_results_0(ggml_backend_vk_context * ctx, ggml_cgraph *
|
||||
} else if (tensor->op == GGML_OP_RWKV_WKV7) {
|
||||
tensor_clone = ggml_rwkv_wkv7(ggml_ctx, src_clone[0], src_clone[1], src_clone[2], src_clone[3],
|
||||
src_clone[4], src_clone[5], src_clone[6]);
|
||||
} else if (tensor->op == GGML_OP_GATED_LINEAR_ATTN) {
|
||||
const float * op_params = (const float *)tensor->op_params;
|
||||
tensor_clone = ggml_gated_linear_attn(ggml_ctx, src_clone[0], src_clone[1],
|
||||
src_clone[2], src_clone[3], src_clone[4], op_params[0]);
|
||||
} else if (tensor->op == GGML_OP_GATED_DELTA_NET) {
|
||||
tensor_clone = ggml_gated_delta_net(ggml_ctx, src_clone[0], src_clone[1],
|
||||
src_clone[2], src_clone[3], src_clone[4], src_clone[5],
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
#version 450
|
||||
|
||||
#extension GL_EXT_control_flow_attributes : require
|
||||
|
||||
#define BLOCK_SIZE 64
|
||||
layout(local_size_x = BLOCK_SIZE, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout(push_constant) uniform Parameters {
|
||||
uint B;
|
||||
uint T;
|
||||
uint C;
|
||||
uint H;
|
||||
float scale;
|
||||
};
|
||||
|
||||
layout(binding = 0) readonly buffer KBuf { A_TYPE k[]; };
|
||||
layout(binding = 1) readonly buffer VBuf { A_TYPE v[]; };
|
||||
layout(binding = 2) readonly buffer QBuf { A_TYPE q[]; };
|
||||
layout(binding = 3) readonly buffer GBuf { A_TYPE g[]; };
|
||||
layout(binding = 4) readonly buffer StateBuf { A_TYPE state_in[]; };
|
||||
layout(binding = 5) buffer DstBuf { A_TYPE dst[]; };
|
||||
|
||||
shared A_TYPE _k[BLOCK_SIZE], _q[BLOCK_SIZE], _g[BLOCK_SIZE];
|
||||
|
||||
void main() {
|
||||
const uint head_size = BLOCK_SIZE;
|
||||
const uint batch_id = gl_WorkGroupID.x / H;
|
||||
const uint head_id = gl_WorkGroupID.x % H;
|
||||
const uint tid = gl_LocalInvocationID.x;
|
||||
|
||||
const uint state_size = C * head_size;
|
||||
const uint n_seq_tokens = T / B;
|
||||
|
||||
if (batch_id >= B || head_id >= H) {
|
||||
return;
|
||||
}
|
||||
|
||||
// state[i] holds column tid of this head's state matrix: S[i][tid]
|
||||
A_TYPE state[BLOCK_SIZE];
|
||||
[[unroll]] for (uint i = 0; i < head_size; i++) {
|
||||
state[i] = state_in[batch_id * state_size + head_id * head_size * head_size
|
||||
+ i * head_size + tid];
|
||||
}
|
||||
|
||||
const uint start_t = batch_id * n_seq_tokens * C + head_id * head_size + tid;
|
||||
const uint end_t = (batch_id + 1) * n_seq_tokens * C + head_id * head_size + tid;
|
||||
|
||||
for (uint t = start_t; t < end_t; t += C) {
|
||||
barrier();
|
||||
_k[tid] = k[t];
|
||||
_q[tid] = q[t];
|
||||
_g[tid] = g[t];
|
||||
barrier();
|
||||
|
||||
const A_TYPE v_val = v[t];
|
||||
A_TYPE y = 0.0;
|
||||
|
||||
[[unroll]] for (uint i = 0; i < head_size; i += 4) {
|
||||
vec4 k_vec = vec4(_k[i], _k[i+1], _k[i+2], _k[i+3]);
|
||||
vec4 q_vec = vec4(_q[i], _q[i+1], _q[i+2], _q[i+3]);
|
||||
vec4 g_vec = vec4(_g[i], _g[i+1], _g[i+2], _g[i+3]);
|
||||
vec4 s_vec = vec4(state[i], state[i+1], state[i+2], state[i+3]);
|
||||
|
||||
vec4 kv = k_vec * v_val;
|
||||
|
||||
s_vec = s_vec * g_vec + kv;
|
||||
y += dot(q_vec, s_vec);
|
||||
|
||||
state[i] = s_vec.x;
|
||||
state[i+1] = s_vec.y;
|
||||
state[i+2] = s_vec.z;
|
||||
state[i+3] = s_vec.w;
|
||||
}
|
||||
|
||||
dst[t] = y * scale;
|
||||
}
|
||||
|
||||
[[unroll]] for (uint i = 0; i < head_size; i++) {
|
||||
dst[T * C + batch_id * state_size + head_id * head_size * head_size
|
||||
+ i * head_size + tid] = state[i];
|
||||
}
|
||||
}
|
||||
@@ -1057,6 +1057,8 @@ void process_shaders() {
|
||||
|
||||
string_to_spv("rwkv_wkv6_f32", "wkv6.comp", merge_maps(base_dict, {{"A_TYPE", "float"}}));
|
||||
|
||||
string_to_spv("gated_linear_attn_f32", "gla.comp", merge_maps(base_dict, {{"A_TYPE", "float"}}));
|
||||
|
||||
string_to_spv("rwkv_wkv7_f32", "wkv7.comp", merge_maps(base_dict, {{"A_TYPE", "float"}}));
|
||||
|
||||
string_to_spv("gated_delta_net_f32", "gated_delta_net.comp", merge_maps(base_dict, {{"FLOAT_TYPE", "float"}, {"USE_SUBGROUP_ADD", "1"}, {"USE_SUBGROUP_CLUSTERED", "1"}}));
|
||||
|
||||
@@ -11,6 +11,7 @@ GGUF_MAGIC = 0x46554747 # "GGUF"
|
||||
GGUF_VERSION = 3
|
||||
GGUF_DEFAULT_ALIGNMENT = 32
|
||||
GGML_QUANT_VERSION = 2 # GGML_QNT_VERSION from ggml.h
|
||||
GGML_MAX_DIMS = 4 # GGML_MAX_DIMS from ggml.h
|
||||
|
||||
#
|
||||
# metadata keys
|
||||
@@ -3221,6 +3222,13 @@ 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
|
||||
MODEL_TENSOR.NEXTN_EH_PROJ,
|
||||
MODEL_TENSOR.NEXTN_EMBED_TOKENS,
|
||||
MODEL_TENSOR.NEXTN_ENORM,
|
||||
MODEL_TENSOR.NEXTN_HNORM,
|
||||
MODEL_TENSOR.NEXTN_SHARED_HEAD_HEAD,
|
||||
MODEL_TENSOR.NEXTN_SHARED_HEAD_NORM,
|
||||
],
|
||||
MODEL_ARCH.DEEPSEEK2OCR: [
|
||||
MODEL_TENSOR.TOKEN_EMBD,
|
||||
|
||||
@@ -22,6 +22,7 @@ if __name__ == "__main__":
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from gguf.constants import (
|
||||
GGML_MAX_DIMS,
|
||||
GGML_QUANT_SIZES,
|
||||
GGUF_DEFAULT_ALIGNMENT,
|
||||
GGUF_MAGIC,
|
||||
@@ -266,6 +267,8 @@ class GGUFReader:
|
||||
# Get Tensor Dimensions Count
|
||||
n_dims = self._get(offs, np.uint32)
|
||||
offs += int(n_dims.nbytes)
|
||||
if n_dims[0] > GGML_MAX_DIMS:
|
||||
raise ValueError(f'Tensor dimensions count {n_dims[0]} exceeds GGML_MAX_DIMS ({GGML_MAX_DIMS})')
|
||||
|
||||
# Get Tensor Dimension Array
|
||||
dims = self._get(offs, np.uint64, n_dims[0])
|
||||
@@ -326,7 +329,10 @@ class GGUFReader:
|
||||
raise ValueError(f'Found duplicated tensor with name {tensor_name}')
|
||||
tensor_names.add(tensor_name)
|
||||
ggml_type = GGMLQuantizationType(raw_dtype[0])
|
||||
n_elems = int(np.prod(dims))
|
||||
# use Python ints: np.prod on uint64 wraps silently on overflow
|
||||
n_elems = 1
|
||||
for dim in dims.tolist():
|
||||
n_elems *= int(dim)
|
||||
np_dims = tuple(reversed(dims.tolist()))
|
||||
block_size, type_size = GGML_QUANT_SIZES[ggml_type]
|
||||
n_bytes = n_elems * type_size // block_size
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import struct
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from gguf.gguf_reader import GGUFReader
|
||||
|
||||
|
||||
def _write_gguf(path, n_dims_field, dims):
|
||||
buf = b'GGUF' + struct.pack('<IQQ', 3, 1, 0) # version 3, 1 tensor, 0 kv
|
||||
name = b'bad_tensor'
|
||||
buf += struct.pack('<Q', len(name)) + name
|
||||
buf += struct.pack('<I', n_dims_field)
|
||||
for d in dims:
|
||||
buf += struct.pack('<Q', d)
|
||||
buf += struct.pack('<I', 0) # dtype F32
|
||||
buf += struct.pack('<Q', 0) # tensor offset
|
||||
buf += b'\x00' * 64
|
||||
path.write_bytes(buf)
|
||||
|
||||
|
||||
def test_n_dims_upper_bound(tmp_path):
|
||||
# crafted file claims 1_000_000 dims; must be rejected, not read past EOF
|
||||
p = tmp_path / 'evil_ndims.gguf'
|
||||
_write_gguf(p, 1_000_000, [1] * 8)
|
||||
with pytest.raises(ValueError, match='exceeds GGML_MAX_DIMS'):
|
||||
GGUFReader(p)
|
||||
|
||||
|
||||
def test_dims_product_no_uint64_wraparound(tmp_path):
|
||||
# dims whose true product overflows uint64; np.prod would wrap to 4 and
|
||||
# silently pass an undersized read. The reader must not accept it.
|
||||
dims = [4194305, 4194305, 211106198978564]
|
||||
assert int(np.prod(np.array(dims, dtype=np.uint64))) == 4 # the wrap bug
|
||||
p = tmp_path / 'evil_overflow.gguf'
|
||||
_write_gguf(p, len(dims), dims)
|
||||
with pytest.raises(ValueError):
|
||||
GGUFReader(p)
|
||||
+1
-1
@@ -1256,7 +1256,6 @@ extern "C" {
|
||||
struct ggml_tensor * probs;
|
||||
struct ggml_tensor * sampled;
|
||||
struct ggml_tensor * candidates;
|
||||
int64_t n_vocab;
|
||||
};
|
||||
|
||||
// user code can implement the interface below in order to create custom llama_sampler
|
||||
@@ -1425,6 +1424,7 @@ extern "C" {
|
||||
|
||||
/// NOTE: Avoid using on the full vocabulary as searching for repeated tokens can become slow. For example, apply top-k or top-p sampling first.
|
||||
LLAMA_API struct llama_sampler * llama_sampler_init_penalties(
|
||||
int32_t n_vocab,
|
||||
int32_t penalty_last_n, // last n tokens to penalize (0 = disable penalty, -1 = context size)
|
||||
float penalty_repeat, // must be > 0.0, 1.0 = disabled
|
||||
float penalty_freq, // must be finite, 0.0 = disabled
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
{%- if not add_generation_prompt is defined -%}
|
||||
{%- set add_generation_prompt = false -%}
|
||||
{%- endif -%}
|
||||
{%- if not thinking is defined -%}
|
||||
{%- if enable_thinking is defined -%}
|
||||
{%- set thinking = enable_thinking -%}
|
||||
{%- else -%}
|
||||
{%- set thinking = false -%}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- if not drop_thinking is defined -%}
|
||||
{%- set drop_thinking = true -%}
|
||||
{%- endif -%}
|
||||
{%- set dsml_token = '|DSML|' -%}
|
||||
{%- set thinking_start_token = '<think>' -%}
|
||||
{%- set thinking_end_token = '</think>' -%}
|
||||
{%- set reasoning_effort_high = 'Reasoning Effort: Absolute maximum with no shortcuts permitted.\nYou MUST be very thorough in your thinking and comprehensively decompose the problem to resolve the root cause, rigorously stress-testing your logic against all potential paths, edge cases, and adversarial scenarios.\nExplicitly write out your entire deliberation process, documenting every intermediate step, considered alternative, and rejected hypothesis to ensure absolutely no assumption is left unchecked.\n\n' -%}
|
||||
{%- set reasoning_effort_max = 'Reasoning Effort: Beyond maximum — exhaustive, relentless, and uncompromising.\nYou MUST reason with the utmost depth and rigor, leaving absolutely nothing to chance: exhaustively decompose the problem into its most fundamental components, trace every causal chain to its root, and resolve the underlying cause rather than any surface symptom.\nDo not stop reasoning until you have independently verified the solution from multiple angles and are certain that no assumption remains unchecked and no error remains undiscovered.\n\n' -%}
|
||||
{%- set response_format_template = '## Response Format:\n\nYou MUST strictly adhere to the following schema to reply:\n' -%}
|
||||
{%- set has_tools = false -%}
|
||||
{%- set tools_header = '## Tools\n\nYou have access to a set of tools to help answer the user\'s question. You can invoke tools by writing a "<' + dsml_token + 'tool_calls>" block like the following:\n\n<' + dsml_token + 'tool_calls>\n<' + dsml_token + 'invoke name="$TOOL_NAME">\n<' + dsml_token + 'parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</' + dsml_token + 'parameter>\n...\n</' + dsml_token + 'invoke>\n<' + dsml_token + 'invoke name="$TOOL_NAME2">\n...\n</' + dsml_token + 'invoke>\n</' + dsml_token + 'tool_calls>\n\nString parameters should be specified as is and set `string="true"`. For all other types (numbers, booleans, arrays, objects), pass the value in JSON format and set `string="false"`.\n\nIf thinking_mode is enabled (triggered by ' + thinking_start_token + '), you MUST output your complete reasoning inside ' + thinking_start_token + '...' + thinking_end_token + ' BEFORE any tool calls or final response.\n\nOtherwise, output directly after ' + thinking_end_token + ' with tool calls or final response.\n\n### Available Tool Schemas\n\n' -%}
|
||||
{%- set tools_footer = '\nYou MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.\n' -%}
|
||||
{%- set ns = namespace(system_prompt='', is_first_sp=true, has_tool_calls=false) -%}
|
||||
{%- for message in messages -%}
|
||||
{%- if message['role'] == 'system' -%}
|
||||
{%- if ns.is_first_sp -%}
|
||||
{%- set ns.system_prompt = ns.system_prompt + (message['content'] or '') -%}
|
||||
{%- set ns.is_first_sp = false -%}
|
||||
{%- else -%}
|
||||
{%- set ns.system_prompt = ns.system_prompt + '\n\n' + (message['content'] or '') -%}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- if tools is defined and tools -%}
|
||||
{%- set has_tools = true -%}
|
||||
{%- set ts = namespace(schemas='') -%}
|
||||
{%- for tool in tools -%}
|
||||
{%- if tool['type'] == 'function' -%}
|
||||
{%- set ts.schemas = ts.schemas + (tool['function'] | tojson) + '\n' -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- if ns.system_prompt -%}
|
||||
{%- set ns.system_prompt = ns.system_prompt + '\n\n' + tools_header + ts.schemas + tools_footer -%}
|
||||
{%- else -%}
|
||||
{%- set ns.system_prompt = tools_header + ts.schemas + tools_footer -%}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- if response_format is defined -%}
|
||||
{%- if ns.system_prompt -%}
|
||||
{%- set ns.system_prompt = ns.system_prompt + '\n\n' -%}
|
||||
{%- endif -%}
|
||||
{%- set ns.system_prompt = ns.system_prompt + response_format_template + (response_format | tojson) -%}
|
||||
{%- endif -%}
|
||||
{{- bos_token -}}
|
||||
{%- if messages and thinking and reasoning_effort is defined and reasoning_effort == 'high' -%}
|
||||
{{- reasoning_effort_high -}}
|
||||
{%- elif messages and thinking and reasoning_effort is defined and reasoning_effort == 'max' -%}
|
||||
{{- reasoning_effort_max -}}
|
||||
{%- endif -%}
|
||||
{{- ns.system_prompt -}}
|
||||
{%- set last_user_idx = namespace(value=-1) -%}
|
||||
{%- for message in messages -%}
|
||||
{%- if message['role'] == 'user' or message['role'] == 'developer' or message['role'] == 'tool' -%}
|
||||
{%- set last_user_idx.value = loop.index0 -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- set state = namespace(in_user=false) -%}
|
||||
{%- for message in messages -%}
|
||||
{%- if message['role'] == 'tool' -%}
|
||||
{%- set ns.has_tool_calls = true -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- for message in messages -%}
|
||||
{%- if message['role'] == 'user' or message['role'] == 'developer' -%}
|
||||
{%- if state.in_user -%}
|
||||
{{- '\n\n' -}}
|
||||
{%- else -%}
|
||||
{{- '<|User|>' -}}
|
||||
{%- set state.in_user = true -%}
|
||||
{%- endif -%}
|
||||
{{- message['content'] or '' -}}
|
||||
{%- elif message['role'] == 'tool' -%}
|
||||
{%- if state.in_user -%}
|
||||
{{- '\n\n' -}}
|
||||
{%- else -%}
|
||||
{{- '<|User|>' -}}
|
||||
{%- set state.in_user = true -%}
|
||||
{%- endif -%}
|
||||
{{- '<tool_result>' + (message['content'] or '') + '</tool_result>' -}}
|
||||
{%- elif message['role'] == 'assistant' -%}
|
||||
{%- set state.in_user = false -%}
|
||||
{{- '<|Assistant|>' -}}
|
||||
{%- set is_after_last_user = loop.index0 > last_user_idx.value -%}
|
||||
{%- set keep_reasoning = thinking and ((not drop_thinking) or has_tools or is_after_last_user or ns.has_tool_calls) -%}
|
||||
{%- if keep_reasoning -%}
|
||||
{{- thinking_start_token -}}
|
||||
{%- if message['reasoning_content'] is defined and message['reasoning_content'] -%}
|
||||
{{- message['reasoning_content'] -}}
|
||||
{%- endif -%}
|
||||
{{- thinking_end_token -}}
|
||||
{%- else -%}
|
||||
{{- thinking_end_token -}}
|
||||
{%- endif -%}
|
||||
{%- if message['content'] is defined and message['content'] -%}
|
||||
{{- message['content'] -}}
|
||||
{%- endif -%}
|
||||
{%- if message['tool_calls'] -%}
|
||||
{{- '\n\n<' + dsml_token + 'tool_calls>\n' -}}
|
||||
{%- for tool in message['tool_calls'] -%}
|
||||
{%- set func = tool['function'] -%}
|
||||
{{- '<' + dsml_token + 'invoke name="' + func['name'] + '">\n' -}}
|
||||
{%- set args = func['arguments'] -%}
|
||||
{%- if args is string -%}
|
||||
{%- set args = args | from_json -%}
|
||||
{%- endif -%}
|
||||
{%- for key, val in args.items() -%}
|
||||
{%- if val is string -%}
|
||||
{{- '<' + dsml_token + 'parameter name="' + key + '" string="true">' + val + '</' + dsml_token + 'parameter>\n' -}}
|
||||
{%- else -%}
|
||||
{{- '<' + dsml_token + 'parameter name="' + key + '" string="false">' + (val | tojson) + '</' + dsml_token + 'parameter>\n' -}}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- if not args -%}
|
||||
{{- '\n' -}}
|
||||
{%- endif -%}
|
||||
{{- '</' + dsml_token + 'invoke>\n' -}}
|
||||
{%- endfor -%}
|
||||
{{- '</' + dsml_token + 'tool_calls>' -}}
|
||||
{%- endif -%}
|
||||
{{- '<|end▁of▁sentence|>' -}}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- if add_generation_prompt -%}
|
||||
{{- '<|Assistant|>' -}}
|
||||
{%- if thinking -%}
|
||||
{{- thinking_start_token -}}
|
||||
{%- else -%}
|
||||
{{- thinking_end_token -}}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
@@ -9,11 +9,14 @@
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- if not drop_thinking is defined -%}
|
||||
{%- set drop_thinking = false -%}
|
||||
{%- set drop_thinking = true -%}
|
||||
{%- endif -%}
|
||||
{%- set dsml_token = '|DSML|' -%}
|
||||
{%- set thinking_start_token = '<think>' -%}
|
||||
{%- set thinking_end_token = '</think>' -%}
|
||||
{%- set reasoning_effort_max = 'Reasoning Effort: Absolute maximum with no shortcuts permitted.\nYou MUST be very thorough in your thinking and comprehensively decompose the problem to resolve the root cause, rigorously stress-testing your logic against all potential paths, edge cases, and adversarial scenarios.\nExplicitly write out your entire deliberation process, documenting every intermediate step, considered alternative, and rejected hypothesis to ensure absolutely no assumption is left unchecked.\n\n' -%}
|
||||
{%- set response_format_template = '## Response Format:\n\nYou MUST strictly adhere to the following schema to reply:\n' -%}
|
||||
{%- set has_tools = false -%}
|
||||
{%- set tools_header = '## Tools\n\nYou have access to a set of tools to help answer the user\'s question. You can invoke tools by writing a "<' + dsml_token + 'tool_calls>" block like the following:\n\n<' + dsml_token + 'tool_calls>\n<' + dsml_token + 'invoke name="$TOOL_NAME">\n<' + dsml_token + 'parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</' + dsml_token + 'parameter>\n...\n</' + dsml_token + 'invoke>\n<' + dsml_token + 'invoke name="$TOOL_NAME2">\n...\n</' + dsml_token + 'invoke>\n</' + dsml_token + 'tool_calls>\n\nString parameters should be specified as is and set `string="true"`. For all other types (numbers, booleans, arrays, objects), pass the value in JSON format and set `string="false"`.\n\nIf thinking_mode is enabled (triggered by ' + thinking_start_token + '), you MUST output your complete reasoning inside ' + thinking_start_token + '...' + thinking_end_token + ' BEFORE any tool calls or final response.\n\nOtherwise, output directly after ' + thinking_end_token + ' with tool calls or final response.\n\n### Available Tool Schemas\n\n' -%}
|
||||
{%- set tools_footer = '\nYou MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.\n' -%}
|
||||
{%- set ns = namespace(system_prompt='', is_first_sp=true, has_tool_calls=false) -%}
|
||||
@@ -28,6 +31,7 @@
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- if tools is defined and tools -%}
|
||||
{%- set has_tools = true -%}
|
||||
{%- set ts = namespace(schemas='') -%}
|
||||
{%- for tool in tools -%}
|
||||
{%- if tool['type'] == 'function' -%}
|
||||
@@ -40,7 +44,16 @@
|
||||
{%- set ns.system_prompt = tools_header + ts.schemas + tools_footer -%}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- if response_format is defined -%}
|
||||
{%- if ns.system_prompt -%}
|
||||
{%- set ns.system_prompt = ns.system_prompt + '\n\n' -%}
|
||||
{%- endif -%}
|
||||
{%- set ns.system_prompt = ns.system_prompt + response_format_template + (response_format | tojson) -%}
|
||||
{%- endif -%}
|
||||
{{- bos_token -}}
|
||||
{%- if messages and thinking and reasoning_effort is defined and reasoning_effort == 'max' -%}
|
||||
{{- reasoning_effort_max -}}
|
||||
{%- endif -%}
|
||||
{{- ns.system_prompt -}}
|
||||
{%- set last_user_idx = namespace(value=-1) -%}
|
||||
{%- for message in messages -%}
|
||||
@@ -75,8 +88,8 @@
|
||||
{%- set state.in_user = false -%}
|
||||
{{- '<|Assistant|>' -}}
|
||||
{%- set is_after_last_user = loop.index0 > last_user_idx.value -%}
|
||||
{%- set retain_reasoning = (not drop_thinking) or (is_after_last_user or ns.has_tool_calls) -%}
|
||||
{%- if retain_reasoning and thinking -%}
|
||||
{%- set keep_reasoning = thinking and ((not drop_thinking) or has_tools or is_after_last_user or ns.has_tool_calls) -%}
|
||||
{%- if keep_reasoning -%}
|
||||
{{- thinking_start_token -}}
|
||||
{%- if message['reasoning_content'] is defined and message['reasoning_content'] -%}
|
||||
{{- message['reasoning_content'] -}}
|
||||
@@ -104,6 +117,9 @@
|
||||
{{- '<' + dsml_token + 'parameter name="' + key + '" string="false">' + (val | tojson) + '</' + dsml_token + 'parameter>\n' -}}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- if not args -%}
|
||||
{{- '\n' -}}
|
||||
{%- endif -%}
|
||||
{{- '</' + dsml_token + 'invoke>\n' -}}
|
||||
{%- endfor -%}
|
||||
{{- '</' + dsml_token + 'tool_calls>' -}}
|
||||
@@ -118,4 +134,4 @@
|
||||
{%- else -%}
|
||||
{{- thinking_end_token -}}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
|
||||
@@ -1 +1 @@
|
||||
06ca97616793248fadb410ea8d69c7511b2005e4
|
||||
90951f99af1fbebef3fbdd58ff5b8715b0bb9c43
|
||||
|
||||
@@ -5,7 +5,7 @@ import os
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
HTTPLIB_VERSION = "refs/tags/v0.51.0"
|
||||
HTTPLIB_VERSION = "refs/tags/v0.52.0"
|
||||
|
||||
vendor = {
|
||||
"https://github.com/nlohmann/json/releases/latest/download/json.hpp": "vendor/nlohmann/json.hpp",
|
||||
|
||||
@@ -3683,7 +3683,6 @@ void llm_graph_context::build_sampling() const {
|
||||
/*.probs =*/ nullptr,
|
||||
/*.sampled =*/ nullptr,
|
||||
/*.candidates =*/ nullptr,
|
||||
/*.n_vocab =*/ logits_seq->ne[0],
|
||||
};
|
||||
|
||||
assert(sampler->iface->backend_apply);
|
||||
|
||||
+47
-52
@@ -857,7 +857,11 @@ struct ggml_tensor * llama_model_loader::require_tensor_meta(const std::string &
|
||||
return tensor;
|
||||
}
|
||||
|
||||
const struct ggml_tensor * llama_model_loader::check_tensor_dims(const std::string & name, const std::vector<int64_t> & ne, bool required) const {
|
||||
const struct ggml_tensor * llama_model_loader::check_tensor_dims(
|
||||
const std::string & name,
|
||||
const std::vector<int64_t> & ne,
|
||||
bool required,
|
||||
bool allow_reshape) const {
|
||||
const struct ggml_tensor * cur = get_tensor_meta(name.c_str());
|
||||
|
||||
if (cur == NULL) {
|
||||
@@ -867,21 +871,33 @@ const struct ggml_tensor * llama_model_loader::check_tensor_dims(const std::stri
|
||||
throw std::runtime_error(format("%s: tensor '%s' not found", __func__, name.c_str()));
|
||||
}
|
||||
|
||||
{
|
||||
bool is_ok = true;
|
||||
bool is_ok = true;
|
||||
|
||||
if (allow_reshape) {
|
||||
// check total number of elements only
|
||||
const int64_t ncur = ggml_nelements(cur);
|
||||
int64_t nexp = 1;
|
||||
for (size_t i = 0; i < ne.size(); ++i) {
|
||||
nexp *= ne[i];
|
||||
}
|
||||
if (ncur != nexp) {
|
||||
is_ok = false;
|
||||
}
|
||||
} else {
|
||||
for (size_t i = 0; i < GGML_MAX_DIMS; ++i) {
|
||||
if ((i < ne.size() && ne[i] != cur->ne[i]) || (i >= ne.size() && cur->ne[i] != 1)) {
|
||||
is_ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!is_ok) {
|
||||
throw std::runtime_error(
|
||||
format("%s: tensor '%s' has wrong shape; expected %s, got %s",
|
||||
__func__, name.c_str(),
|
||||
llama_format_tensor_shape(ne).c_str(),
|
||||
llama_format_tensor_shape(cur).c_str()));
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_ok) {
|
||||
throw std::runtime_error(
|
||||
format("%s: tensor '%s' has wrong shape; expected %s, got %s",
|
||||
__func__, name.c_str(),
|
||||
llama_format_tensor_shape(ne).c_str(),
|
||||
llama_format_tensor_shape(cur).c_str()));
|
||||
}
|
||||
|
||||
return cur;
|
||||
@@ -1246,11 +1262,25 @@ struct ggml_tensor * llama_model_loader::create_tensor(
|
||||
return ret;
|
||||
}
|
||||
|
||||
ggml_tensor * t_meta = get_tensor_meta(tn.str().c_str());
|
||||
ggml_backend_buffer_type_t buft = buft_for_tensor(t_meta);
|
||||
if (buft == nullptr) {
|
||||
return nullptr; // return type is ggml_tensor *
|
||||
LLAMA_LOG_DEBUG("%s: loading tensor %s\n", __func__, tn.str().c_str());
|
||||
const struct ggml_tensor * cur = check_tensor_dims(tn.str(), ne, !(flags & TENSOR_NOT_REQUIRED), flags & TENSOR_ALLOW_RESHAPE);
|
||||
if (cur == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ggml_tensor t_meta = *cur;
|
||||
if (flags & TENSOR_ALLOW_RESHAPE) {
|
||||
for (size_t dim = 0; dim < GGML_MAX_DIMS; dim++) {
|
||||
t_meta.ne[dim] = dim < ne.size() ? ne.begin()[dim] : 1;
|
||||
t_meta.nb[dim] = dim == 0 ? ggml_type_size(t_meta.type) : t_meta.ne[dim-1]*t_meta.nb[dim-1];
|
||||
}
|
||||
}
|
||||
|
||||
ggml_backend_buffer_type_t buft = buft_for_tensor(&t_meta);
|
||||
if (buft == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ggml_context * ctx = ctx_for_buft(buft);
|
||||
|
||||
// if duplicated, check if the original tensor was allocated in the same buffer type context and avoid creating a new one
|
||||
@@ -1261,20 +1291,13 @@ struct ggml_tensor * llama_model_loader::create_tensor(
|
||||
}
|
||||
}
|
||||
|
||||
LLAMA_LOG_DEBUG("%s: loading tensor %s\n", __func__, tn.str().c_str());
|
||||
const struct ggml_tensor * cur = check_tensor_dims(tn.str(), ne, !(flags & TENSOR_NOT_REQUIRED));
|
||||
|
||||
if (cur == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const bool duplicated = flags & TENSOR_DUPLICATED;
|
||||
|
||||
struct ggml_tensor * tensor = ggml_dup_tensor(ctx, cur);
|
||||
ggml_set_name(tensor, ggml_get_name(cur));
|
||||
struct ggml_tensor * tensor = ggml_dup_tensor(ctx, &t_meta);
|
||||
ggml_set_name(tensor, ggml_get_name(&t_meta));
|
||||
|
||||
if (duplicated) {
|
||||
size_data += ggml_nbytes(cur);
|
||||
size_data += ggml_nbytes(&t_meta);
|
||||
} else {
|
||||
n_created++;
|
||||
}
|
||||
@@ -1282,34 +1305,6 @@ struct ggml_tensor * llama_model_loader::create_tensor(
|
||||
return tensor;
|
||||
}
|
||||
|
||||
struct ggml_tensor * llama_model_loader::create_tensor_as_view(struct ggml_context * ctx, struct ggml_tensor * base, const std::string & name, const std::initializer_list<int64_t> & ne, size_t offset, bool required) {
|
||||
const struct ggml_tensor * cur = check_tensor_dims(name, ne, required);
|
||||
|
||||
if (cur == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (cur->type != base->type) {
|
||||
throw std::runtime_error(format("%s: tensor '%s' has wrong type; expected %s, got %s", __func__, name.c_str(), ggml_type_name(base->type), ggml_type_name(cur->type)));
|
||||
}
|
||||
|
||||
std::array<int64_t, GGML_MAX_DIMS> dims;
|
||||
for (size_t i = 0; i < GGML_MAX_DIMS; ++i) {
|
||||
dims[i] = i < ne.size() ? ne.begin()[i] : 1;
|
||||
}
|
||||
|
||||
struct ggml_tensor * tensor = ggml_view_4d(ctx, base,
|
||||
dims[0], dims[1], dims[2], dims[3],
|
||||
cur->nb[1], cur->nb[2], cur->nb[3],
|
||||
offset);
|
||||
|
||||
ggml_set_name(tensor, name.c_str());
|
||||
|
||||
n_created++;
|
||||
|
||||
return tensor;
|
||||
}
|
||||
|
||||
void llama_model_loader::done_getting_tensors(bool partial) const {
|
||||
if (n_created > n_tensors) {
|
||||
throw std::runtime_error(format("%s: too many tensors created; expected %d, got %d", __func__, n_tensors, n_created));
|
||||
|
||||
@@ -67,6 +67,7 @@ struct llama_model_loader {
|
||||
static const int TENSOR_DUPLICATED = 1 << 1;
|
||||
static const int TENSOR_SKIP = 1 << 2;
|
||||
static const int TENSOR_SKIP_IF_VIRTUAL = 1 << 3;
|
||||
static const int TENSOR_ALLOW_RESHAPE = 1 << 4;
|
||||
|
||||
int n_kv = 0;
|
||||
int n_tensors = 0;
|
||||
@@ -177,14 +178,16 @@ struct llama_model_loader {
|
||||
|
||||
struct ggml_tensor * require_tensor_meta(const std::string & name) const;
|
||||
|
||||
const struct ggml_tensor * check_tensor_dims(const std::string & name, const std::vector<int64_t> & ne, bool required) const;
|
||||
const struct ggml_tensor * check_tensor_dims(
|
||||
const std::string & name,
|
||||
const std::vector<int64_t> & ne,
|
||||
bool required,
|
||||
bool allow_reshape) const;
|
||||
|
||||
struct ggml_tensor * create_tensor(
|
||||
const llama_hparams & hparams, const buft_list_t * buft_list_cpu, const buft_list_t * buft_list_input, const buft_list_t * buft_list_output,
|
||||
const buft_list_t * buft_list_layer, const LLM_TN_IMPL & tn, const std::initializer_list<int64_t> & ne, int flags);
|
||||
|
||||
struct ggml_tensor * create_tensor_as_view(struct ggml_context * ctx, struct ggml_tensor * base, const std::string & name, const std::initializer_list<int64_t> & ne, size_t offset, bool required = true);
|
||||
|
||||
void done_getting_tensors(bool partial = false) const;
|
||||
|
||||
void init_mappings(bool prefetch = true, llama_mlocks * mlock_mmaps = nullptr);
|
||||
|
||||
+2
-1
@@ -2867,7 +2867,8 @@ llama_model_base::llama_model_base(const struct llama_model_params & params) : l
|
||||
TENSOR_DUPLICATED (llama_model_loader::TENSOR_DUPLICATED),
|
||||
TENSOR_NOT_REQUIRED (llama_model_loader::TENSOR_NOT_REQUIRED),
|
||||
TENSOR_SKIP (llama_model_loader::TENSOR_SKIP),
|
||||
TENSOR_SKIP_IF_VIRTUAL(llama_model_loader::TENSOR_SKIP_IF_VIRTUAL) {}
|
||||
TENSOR_SKIP_IF_VIRTUAL(llama_model_loader::TENSOR_SKIP_IF_VIRTUAL),
|
||||
TENSOR_ALLOW_RESHAPE (llama_model_loader::TENSOR_ALLOW_RESHAPE) {}
|
||||
|
||||
ggml_tensor * llama_model_base::create_tensor(const LLM_TN_IMPL & tn, const std::initializer_list<int64_t> & ne, int flags) {
|
||||
GGML_ASSERT(ml != nullptr);
|
||||
|
||||
@@ -719,6 +719,7 @@ struct llama_model_base : public llama_model {
|
||||
const int TENSOR_NOT_REQUIRED;
|
||||
const int TENSOR_SKIP;
|
||||
const int TENSOR_SKIP_IF_VIRTUAL;
|
||||
const int TENSOR_ALLOW_RESHAPE;
|
||||
|
||||
explicit llama_model_base(const llama_model_params & params);
|
||||
virtual ~llama_model_base() = default;
|
||||
|
||||
@@ -589,7 +589,6 @@ static bool llama_sampler_backend_support(
|
||||
/*.probs = */ nullptr,
|
||||
/*.sampled = */ nullptr,
|
||||
/*.candidates = */ ggml_new_tensor_1d(ctx, GGML_TYPE_I32, n),
|
||||
/*.n_vocab = */ n,
|
||||
};
|
||||
|
||||
ggml_cgraph * gf = ggml_new_graph(ctx);
|
||||
@@ -2640,6 +2639,7 @@ struct llama_sampler * llama_sampler_init_grammar_lazy_patterns(
|
||||
// penalties
|
||||
|
||||
struct llama_sampler_penalties : public llama_sampler_backend {
|
||||
const int32_t n_vocab;
|
||||
const int32_t penalty_last_n;
|
||||
const float penalty_repeat;
|
||||
const float penalty_freq;
|
||||
@@ -2655,7 +2655,6 @@ struct llama_sampler_penalties : public llama_sampler_backend {
|
||||
ggml_tensor * inp_counts = nullptr;
|
||||
|
||||
// backend helpers
|
||||
int32_t n_vocab = 0;
|
||||
int32_t n_max = 0;
|
||||
bool has_candidates = false;
|
||||
|
||||
@@ -2676,11 +2675,13 @@ struct llama_sampler_penalties : public llama_sampler_backend {
|
||||
}
|
||||
|
||||
llama_sampler_penalties(
|
||||
int32_t n_vocab,
|
||||
int32_t penalty_last_n,
|
||||
float penalty_repeat,
|
||||
float penalty_freq,
|
||||
float penalty_present)
|
||||
: llama_sampler_backend("penalties")
|
||||
, n_vocab (n_vocab)
|
||||
, penalty_last_n (penalty_last_n)
|
||||
, penalty_repeat (penalty_repeat)
|
||||
, penalty_freq (penalty_freq)
|
||||
@@ -2766,6 +2767,7 @@ static void llama_sampler_penalties_reset(struct llama_sampler * smpl) {
|
||||
static struct llama_sampler * llama_sampler_penalties_clone(const struct llama_sampler * smpl) {
|
||||
const auto * ctx = (const llama_sampler_penalties *) smpl->ctx;
|
||||
auto * result = llama_sampler_init_penalties(
|
||||
ctx->n_vocab,
|
||||
ctx->penalty_last_n,
|
||||
ctx->penalty_repeat,
|
||||
ctx->penalty_freq,
|
||||
@@ -2811,10 +2813,9 @@ static void llama_sampler_penalties_backend_apply(
|
||||
return;
|
||||
}
|
||||
|
||||
GGML_ASSERT(data->n_vocab > 0 && data->n_vocab <= INT32_MAX);
|
||||
GGML_ASSERT(sctx->n_vocab > 0);
|
||||
|
||||
sctx->has_candidates = data->candidates != nullptr;
|
||||
sctx->n_vocab = (int32_t) data->n_vocab;
|
||||
sctx->n_max = std::min(sctx->penalty_last_n, sctx->n_vocab);
|
||||
|
||||
sctx->inp_token_ids = ggml_new_tensor_1d(ctx, GGML_TYPE_I32, sctx->n_max);
|
||||
@@ -2965,6 +2966,7 @@ static struct llama_sampler_i llama_sampler_penalties_i = {
|
||||
};
|
||||
|
||||
struct llama_sampler * llama_sampler_init_penalties(
|
||||
int32_t n_vocab,
|
||||
int32_t penalty_last_n,
|
||||
float penalty_repeat,
|
||||
float penalty_freq,
|
||||
@@ -2979,6 +2981,7 @@ struct llama_sampler * llama_sampler_init_penalties(
|
||||
return llama_sampler_init(
|
||||
/* .iface = */ &llama_sampler_penalties_i,
|
||||
/* .ctx = */ new llama_sampler_penalties(
|
||||
n_vocab,
|
||||
penalty_last_n,
|
||||
penalty_repeat,
|
||||
penalty_freq,
|
||||
|
||||
+18
-7
@@ -1373,8 +1373,10 @@ struct llm_tokenizer_plamo2 : llm_tokenizer {
|
||||
if (vocab.is_byte(token_id)) {
|
||||
if (entry.text.length() == 6 && entry.text.substr(0, 3) == "<0x" && entry.text.back() == '>') {
|
||||
std::string hex_str = entry.text.substr(3, 2);
|
||||
int byte_val = std::stoi(hex_str, nullptr, 16);
|
||||
bytes_[byte_val] = static_cast<llama_token>(token_id);
|
||||
if (std::isxdigit(static_cast<unsigned char>(hex_str[0])) && std::isxdigit(static_cast<unsigned char>(hex_str[1]))) {
|
||||
int byte_val = std::stoi(hex_str, nullptr, 16);
|
||||
bytes_[byte_val] = static_cast<llama_token>(token_id);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -2532,6 +2534,12 @@ void llama_vocab::impl::load(llama_model_loader & ml, const LLM_KV & kv) {
|
||||
const std::string & key = kv(std::get<0>(it));
|
||||
int32_t & id = std::get<1>(it);
|
||||
|
||||
if (id >= 0 && static_cast<size_t>(id) >= id_to_token.size()) {
|
||||
LLAMA_LOG_WARN("%s: default special token '%s' = %d out of vocab range, disabling\n",
|
||||
__func__, key.c_str(), id);
|
||||
id = LLAMA_TOKEN_NULL;
|
||||
}
|
||||
|
||||
uint32_t new_id;
|
||||
if (!ml.get_key(std::get<0>(it), new_id, false)) {
|
||||
continue;
|
||||
@@ -3619,12 +3627,15 @@ int32_t llama_vocab::impl::token_to_piece(llama_token token, char * buf, int32_t
|
||||
if (vocab.is_byte(token)) {
|
||||
// Handle byte tokens like <0xXX>
|
||||
if (token_text.length() == 6 && token_text.substr(0, 3) == "<0x" && token_text.back() == '>') {
|
||||
int hex_val = std::stoi(token_text.substr(3, 2), nullptr, 16);
|
||||
if (length < 1) {
|
||||
return -1;
|
||||
std::string hex_str = token_text.substr(3, 2);
|
||||
if (std::isxdigit(static_cast<unsigned char>(hex_str[0])) && std::isxdigit(static_cast<unsigned char>(hex_str[1]))) {
|
||||
int hex_val = std::stoi(hex_str, nullptr, 16);
|
||||
if (length < 1) {
|
||||
return -1;
|
||||
}
|
||||
buf[0] = static_cast<char>(hex_val);
|
||||
return 1;
|
||||
}
|
||||
buf[0] = static_cast<char>(hex_val);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+308
-25
@@ -37,6 +37,11 @@ void llama_model_deepseek2::load_arch_hparams(llama_model_loader & ml) {
|
||||
hparams.rope_yarn_log_mul /= 0.1f;
|
||||
}
|
||||
|
||||
// NextN/MTP
|
||||
ml.get_key(LLM_KV_NEXTN_PREDICT_LAYERS, hparams.n_layer_nextn, false);
|
||||
GGML_ASSERT(hparams.n_layer_nextn == 0 ||
|
||||
hparams.n_layer() + hparams.n_layer_nextn == hparams.n_layer_all);
|
||||
|
||||
// (optional) temperature tuning - used by mistral-large
|
||||
ml.get_key(LLM_KV_ATTENTION_TEMPERATURE_SCALE, hparams.f_attn_temp_scale, false);
|
||||
ml.get_key(LLM_KV_ATTENTION_TEMPERATURE_LENGTH, hparams.n_attn_temp_floor_scale, false); // FIXME why not use temperature_length?
|
||||
@@ -52,10 +57,20 @@ void llama_model_deepseek2::load_arch_hparams(llama_model_loader & ml) {
|
||||
}
|
||||
}
|
||||
|
||||
void llama_model_deepseek2::load_arch_tensors(llama_model_loader &) {
|
||||
void llama_model_deepseek2::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;
|
||||
}
|
||||
|
||||
const bool is_mla = hparams.is_mla();
|
||||
|
||||
// note: these are the actual head sizes you get when treating as MHA or after "decompression" using wv_b for MLA
|
||||
@@ -81,44 +96,45 @@ void llama_model_deepseek2::load_arch_tensors(llama_model_loader &) {
|
||||
output = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, TENSOR_DUPLICATED);
|
||||
}
|
||||
|
||||
for (int i = 0; i < n_layer; ++i) {
|
||||
for (int i = 0; i < n_layer_all; ++i) {
|
||||
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}, 0);
|
||||
layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i), {n_embd}, flags);
|
||||
if (q_lora_rank > 0) {
|
||||
layer.attn_q_a_norm = create_tensor(tn(LLM_TENSOR_ATTN_Q_A_NORM, "weight", i), {q_lora_rank}, 0);
|
||||
layer.attn_q_a_norm = create_tensor(tn(LLM_TENSOR_ATTN_Q_A_NORM, "weight", i), {q_lora_rank}, flags);
|
||||
}
|
||||
|
||||
layer.attn_kv_a_norm = create_tensor(tn(LLM_TENSOR_ATTN_KV_A_NORM, "weight", i), {kv_lora_rank}, 0);
|
||||
layer.attn_kv_a_norm = create_tensor(tn(LLM_TENSOR_ATTN_KV_A_NORM, "weight", i), {kv_lora_rank}, flags);
|
||||
|
||||
if (q_lora_rank > 0) {
|
||||
layer.wq_a = create_tensor(tn(LLM_TENSOR_ATTN_Q_A, "weight", i), {n_embd, q_lora_rank}, 0);
|
||||
layer.wq_b = create_tensor(tn(LLM_TENSOR_ATTN_Q_B, "weight", i), {q_lora_rank, n_head * n_embd_head_k_mla}, 0);
|
||||
layer.wq_a = create_tensor(tn(LLM_TENSOR_ATTN_Q_A, "weight", i), {n_embd, q_lora_rank}, flags);
|
||||
layer.wq_b = create_tensor(tn(LLM_TENSOR_ATTN_Q_B, "weight", i), {q_lora_rank, n_head * n_embd_head_k_mla}, flags);
|
||||
} else {
|
||||
layer.wq = create_tensor(tn(LLM_TENSOR_ATTN_Q, "weight", i), {n_embd, n_head * n_embd_head_k_mla}, 0);
|
||||
layer.wq = create_tensor(tn(LLM_TENSOR_ATTN_Q, "weight", i), {n_embd, n_head * n_embd_head_k_mla}, flags);
|
||||
}
|
||||
|
||||
layer.wkv_a_mqa = create_tensor(tn(LLM_TENSOR_ATTN_KV_A_MQA, "weight", i), {n_embd, kv_lora_rank + n_embd_head_qk_rope}, 0);
|
||||
layer.wkv_a_mqa = create_tensor(tn(LLM_TENSOR_ATTN_KV_A_MQA, "weight", i), {n_embd, kv_lora_rank + n_embd_head_qk_rope}, flags);
|
||||
|
||||
// note: only old legacy GGUF files will have the unsplit wkv_b tensor in
|
||||
if (is_mla) {
|
||||
layer.wk_b = create_tensor(tn(LLM_TENSOR_ATTN_K_B, "weight", i), {n_embd_head_qk_nope, kv_lora_rank, n_head}, 0);
|
||||
layer.wv_b = create_tensor(tn(LLM_TENSOR_ATTN_V_B, "weight", i), {kv_lora_rank, n_embd_head_v_mla, n_head}, 0);
|
||||
layer.wk_b = create_tensor(tn(LLM_TENSOR_ATTN_K_B, "weight", i), {n_embd_head_qk_nope, kv_lora_rank, n_head}, flags);
|
||||
layer.wv_b = create_tensor(tn(LLM_TENSOR_ATTN_V_B, "weight", i), {kv_lora_rank, n_embd_head_v_mla, n_head}, flags);
|
||||
} else {
|
||||
layer.wkv_b = create_tensor(tn(LLM_TENSOR_ATTN_KV_B, "weight", i), {kv_lora_rank, n_head * (n_embd_head_qk_nope + n_embd_head_v_mla)}, 0);
|
||||
layer.wkv_b = create_tensor(tn(LLM_TENSOR_ATTN_KV_B, "weight", i), {kv_lora_rank, n_head * (n_embd_head_qk_nope + n_embd_head_v_mla)}, flags);
|
||||
}
|
||||
|
||||
layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", i), {n_head * n_embd_head_v_mla, n_embd}, 0);
|
||||
layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", i), {n_head * n_embd_head_v_mla, n_embd}, flags);
|
||||
|
||||
layer.ffn_norm = create_tensor(tn(LLM_TENSOR_FFN_NORM, "weight", i), {n_embd}, 0);
|
||||
layer.ffn_norm = create_tensor(tn(LLM_TENSOR_FFN_NORM, "weight", i), {n_embd}, flags);
|
||||
|
||||
if (i < (int) hparams.n_layer_dense_lead) {
|
||||
layer.ffn_gate = create_tensor(tn(LLM_TENSOR_FFN_GATE, "weight", i), {n_embd, n_ff}, 0);
|
||||
layer.ffn_down = create_tensor(tn(LLM_TENSOR_FFN_DOWN, "weight", i), { n_ff, n_embd}, 0);
|
||||
layer.ffn_up = create_tensor(tn(LLM_TENSOR_FFN_UP, "weight", i), {n_embd, n_ff}, 0);
|
||||
layer.ffn_gate = create_tensor(tn(LLM_TENSOR_FFN_GATE, "weight", i), {n_embd, n_ff}, flags);
|
||||
layer.ffn_down = create_tensor(tn(LLM_TENSOR_FFN_DOWN, "weight", i), { n_ff, n_embd}, flags);
|
||||
layer.ffn_up = create_tensor(tn(LLM_TENSOR_FFN_UP, "weight", i), {n_embd, n_ff}, flags);
|
||||
} else {
|
||||
layer.ffn_gate_inp = create_tensor(tn(LLM_TENSOR_FFN_GATE_INP, "weight", i), {n_embd, n_expert}, 0);
|
||||
layer.ffn_exp_probs_b = create_tensor(tn(LLM_TENSOR_FFN_EXP_PROBS_B, "bias", i), {n_expert}, TENSOR_NOT_REQUIRED);
|
||||
layer.ffn_gate_inp = create_tensor(tn(LLM_TENSOR_FFN_GATE_INP, "weight", i), {n_embd, n_expert}, flags);
|
||||
layer.ffn_exp_probs_b = create_tensor(tn(LLM_TENSOR_FFN_EXP_PROBS_B, "bias", i), {n_expert}, TENSOR_NOT_REQUIRED | flags);
|
||||
|
||||
if (n_expert == 0) {
|
||||
throw std::runtime_error("n_expert must be > 0");
|
||||
@@ -128,21 +144,281 @@ void llama_model_deepseek2::load_arch_tensors(llama_model_loader &) {
|
||||
}
|
||||
|
||||
// MoE branch
|
||||
layer.ffn_down_exps = create_tensor(tn(LLM_TENSOR_FFN_DOWN_EXPS, "weight", i), {n_ff_exp, n_embd, n_expert}, 0);
|
||||
create_tensor_gate_up_exps(layer, i, n_embd, n_ff_exp, n_expert, 0);
|
||||
layer.ffn_down_exps = create_tensor(tn(LLM_TENSOR_FFN_DOWN_EXPS, "weight", i), {n_ff_exp, n_embd, n_expert}, flags);
|
||||
create_tensor_gate_up_exps(layer, i, n_embd, n_ff_exp, n_expert, flags);
|
||||
|
||||
// Shared expert branch
|
||||
layer.ffn_gate_shexp = create_tensor(tn(LLM_TENSOR_FFN_GATE_SHEXP, "weight", i), {n_embd, n_ff_exp * n_expert_shared}, 0);
|
||||
layer.ffn_down_shexp = create_tensor(tn(LLM_TENSOR_FFN_DOWN_SHEXP, "weight", i), { n_ff_exp * n_expert_shared, n_embd}, 0);
|
||||
layer.ffn_up_shexp = create_tensor(tn(LLM_TENSOR_FFN_UP_SHEXP, "weight", i), {n_embd, n_ff_exp * n_expert_shared}, 0);
|
||||
layer.ffn_gate_shexp = create_tensor(tn(LLM_TENSOR_FFN_GATE_SHEXP, "weight", i), {n_embd, n_ff_exp * n_expert_shared}, flags);
|
||||
layer.ffn_down_shexp = create_tensor(tn(LLM_TENSOR_FFN_DOWN_SHEXP, "weight", i), { n_ff_exp * n_expert_shared, n_embd}, flags);
|
||||
layer.ffn_up_shexp = create_tensor(tn(LLM_TENSOR_FFN_UP_SHEXP, "weight", i), {n_embd, n_ff_exp * n_expert_shared}, flags);
|
||||
}
|
||||
|
||||
// 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 }, mtp_flags);
|
||||
layer.nextn.enorm = create_tensor(tn(LLM_TENSOR_NEXTN_ENORM, "weight", i), { n_embd }, mtp_flags);
|
||||
layer.nextn.hnorm = create_tensor(tn(LLM_TENSOR_NEXTN_HNORM, "weight", i), { n_embd }, mtp_flags);
|
||||
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<llm_graph_context> llama_model_deepseek2::build_arch_graph(const llm_graph_params & params) const {
|
||||
if (params.gtype == LLM_GRAPH_TYPE_DECODER_MTP) {
|
||||
return std::make_unique<graph_mtp>(*this, params);
|
||||
}
|
||||
return std::make_unique<graph>(*this, params);
|
||||
}
|
||||
|
||||
llama_model_deepseek2::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 MTP requires n_layer_nextn > 0");
|
||||
GGML_ASSERT(hparams.n_layer_nextn == 1 && "GLM4 MTP currently only supports a single MTP block");
|
||||
GGML_ASSERT(hparams.is_mla() && "GLM4 MTP requires MLA");
|
||||
GGML_ASSERT(hparams.f_attn_temp_scale == 0.0f && "GLM4 MTP does not support attention temperature scaling");
|
||||
|
||||
// The appended MTP block is stored immediately after the main decoder layers.
|
||||
const int il = hparams.n_layer();
|
||||
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((uint32_t) il >= hparams.n_layer_dense_lead && "GLM4 MTP block expected to use MoE FFN");
|
||||
|
||||
const int64_t n_embd_head_k_mla = hparams.n_embd_head_k_mla();
|
||||
const int64_t n_embd_head_qk_rope = hparams.n_rot();
|
||||
const int64_t n_embd_head_qk_nope = n_embd_head_k_mla - n_embd_head_qk_rope;
|
||||
const int64_t kv_lora_rank = hparams.n_lora_kv;
|
||||
|
||||
GGML_ASSERT(n_embd_head_qk_nope >= 1);
|
||||
GGML_ASSERT(hparams.n_lora_q > 0);
|
||||
GGML_ASSERT(layer.wq_a);
|
||||
GGML_ASSERT(layer.attn_q_a_norm);
|
||||
GGML_ASSERT(layer.wq_b);
|
||||
GGML_ASSERT(layer.wkv_a_mqa);
|
||||
GGML_ASSERT(layer.attn_kv_a_norm);
|
||||
GGML_ASSERT(layer.wk_b);
|
||||
|
||||
const bool has_split_exps =
|
||||
layer.ffn_up_exps != nullptr &&
|
||||
layer.ffn_gate_exps != nullptr;
|
||||
|
||||
const bool has_fused_exps = layer.ffn_gate_up_exps != nullptr;
|
||||
|
||||
GGML_ASSERT(has_split_exps || has_fused_exps);
|
||||
GGML_ASSERT(layer.ffn_norm);
|
||||
GGML_ASSERT(layer.ffn_gate_inp);
|
||||
GGML_ASSERT(layer.ffn_down_exps);
|
||||
GGML_ASSERT(layer.ffn_gate_shexp);
|
||||
GGML_ASSERT(layer.ffn_down_shexp);
|
||||
GGML_ASSERT(layer.ffn_up_shexp);
|
||||
|
||||
auto inp = std::make_unique<llm_graph_input_embd_h>(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_k = build_attn_inp_k();
|
||||
|
||||
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);
|
||||
|
||||
ggml_tensor * q = ggml_mul_mat(ctx0, layer.wq_a, cur);
|
||||
cb(q, "mtp_q_a", il);
|
||||
|
||||
q = build_norm(q, layer.attn_q_a_norm, nullptr, LLM_NORM_RMS, il);
|
||||
cb(q, "mtp_q_a_norm", il);
|
||||
|
||||
q = ggml_mul_mat(ctx0, layer.wq_b, q);
|
||||
cb(q, "mtp_q_b", il);
|
||||
|
||||
ggml_tensor * q_nope =
|
||||
ggml_view_3d(ctx0, q, n_embd_head_qk_nope, n_head, n_tokens,
|
||||
ggml_row_size(q->type, n_embd_head_k_mla),
|
||||
ggml_row_size(q->type, n_embd_head_k_mla) * n_head, 0);
|
||||
cb(q_nope, "mtp_q_nope", il);
|
||||
|
||||
ggml_tensor * q_pe =
|
||||
ggml_view_3d(ctx0, q, n_embd_head_qk_rope, n_head, n_tokens,
|
||||
ggml_row_size(q->type, n_embd_head_k_mla),
|
||||
ggml_row_size(q->type, n_embd_head_k_mla) * n_head,
|
||||
ggml_row_size(q->type, n_embd_head_qk_nope));
|
||||
cb(q_pe, "mtp_q_pe", il);
|
||||
|
||||
ggml_tensor * kv_cmpr_pe = ggml_mul_mat(ctx0, layer.wkv_a_mqa, cur);
|
||||
cb(kv_cmpr_pe, "mtp_kv_cmpr_pe", il);
|
||||
|
||||
ggml_tensor * kv_cmpr =
|
||||
ggml_view_2d(ctx0, kv_cmpr_pe, kv_lora_rank, n_tokens,
|
||||
ggml_row_size(kv_cmpr_pe->type, kv_lora_rank + n_embd_head_qk_rope), 0);
|
||||
cb(kv_cmpr, "mtp_kv_cmpr", il);
|
||||
|
||||
ggml_tensor * k_pe =
|
||||
ggml_view_3d(ctx0, kv_cmpr_pe, n_embd_head_qk_rope, 1, n_tokens,
|
||||
ggml_row_size(kv_cmpr_pe->type, kv_lora_rank + n_embd_head_qk_rope),
|
||||
ggml_row_size(kv_cmpr_pe->type, kv_lora_rank + n_embd_head_qk_rope),
|
||||
ggml_row_size(kv_cmpr_pe->type, kv_lora_rank));
|
||||
cb(k_pe, "mtp_k_pe", il);
|
||||
|
||||
kv_cmpr = build_norm(kv_cmpr, layer.attn_kv_a_norm, nullptr, LLM_NORM_RMS, il);
|
||||
cb(kv_cmpr, "mtp_kv_cmpr_norm", il);
|
||||
|
||||
GGML_ASSERT(ext_factor >= 0.0f);
|
||||
|
||||
const float attn_factor_org =
|
||||
attn_factor * (1.0f + 0.1f * logf(1.0f / freq_scale));
|
||||
|
||||
const float mscale =
|
||||
attn_factor_org * (1.0f + 0.1f * hparams.rope_yarn_log_mul * logf(1.0f / freq_scale));
|
||||
|
||||
const float kq_scale =
|
||||
1.0f * mscale * mscale / sqrtf(float(n_embd_head_k_mla));
|
||||
|
||||
q_pe = ggml_rope_ext(ctx0, q_pe, inp_pos, nullptr,
|
||||
n_rot, rope_type, n_ctx_orig, freq_base, freq_scale,
|
||||
ext_factor, attn_factor, beta_fast, beta_slow);
|
||||
cb(q_pe, "mtp_q_pe_rope", il);
|
||||
|
||||
k_pe = ggml_rope_ext(ctx0, k_pe, inp_pos, nullptr,
|
||||
n_rot, rope_type, n_ctx_orig, freq_base, freq_scale,
|
||||
ext_factor, attn_factor, beta_fast, beta_slow);
|
||||
cb(k_pe, "mtp_k_pe_rope", il);
|
||||
|
||||
q_nope = ggml_permute(ctx0, q_nope, 0, 2, 1, 3);
|
||||
cb(q_nope, "mtp_q_nope_perm", il);
|
||||
|
||||
ggml_tensor * q_nope_absorbed = ggml_mul_mat(ctx0, layer.wk_b, q_nope);
|
||||
cb(q_nope_absorbed, "mtp_q_nope_absorbed", il);
|
||||
|
||||
q_nope_absorbed = ggml_permute(ctx0, q_nope_absorbed, 0, 2, 1, 3);
|
||||
cb(q_nope_absorbed, "mtp_q_nope_absorbed_perm", il);
|
||||
|
||||
ggml_tensor * Qcur = ggml_concat(ctx0, q_nope_absorbed, q_pe, 0);
|
||||
cb(Qcur, "mtp_Qcur", il);
|
||||
|
||||
kv_cmpr = ggml_reshape_3d(ctx0, kv_cmpr, hparams.n_lora_kv, 1, n_tokens);
|
||||
cb(kv_cmpr, "mtp_kv_cmpr_reshape", il);
|
||||
|
||||
ggml_tensor * Kcur = ggml_concat(ctx0, kv_cmpr, k_pe, 0);
|
||||
cb(Kcur, "mtp_Kcur", il);
|
||||
|
||||
ggml_tensor * Vcur = kv_cmpr;
|
||||
cb(Vcur, "mtp_Vcur", il);
|
||||
|
||||
cur = build_attn(inp_attn_k,
|
||||
layer.wo, nullptr, layer.wo_s,
|
||||
Qcur, Kcur, Vcur, nullptr, nullptr, layer.wv_b, kq_scale, 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.ffn_norm, nullptr, LLM_NORM_RMS, il);
|
||||
cb(cur, "mtp_ffn_norm", il);
|
||||
|
||||
ggml_tensor * moe_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,
|
||||
nullptr,
|
||||
layer.ffn_gate_up_exps);
|
||||
cb(moe_out, "mtp_ffn_moe_out", il);
|
||||
|
||||
ggml_tensor * ffn_shexp = 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(ffn_shexp, "mtp_ffn_shexp", il);
|
||||
|
||||
cur = ggml_add(ctx0, moe_out, ffn_shexp);
|
||||
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 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 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_deepseek2::graph::graph(const llama_model & model, const llm_graph_params & params) :
|
||||
llm_graph_context(params) {
|
||||
// lite variants include DeepSeek-V2-Lite, GigaChat3-10B-A1.8B
|
||||
@@ -365,7 +641,7 @@ llama_model_deepseek2::graph::graph(const llama_model & model, const llm_graph_p
|
||||
Qcur, Kcur, Vcur, nullptr, nullptr, nullptr, kq_scale, 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);
|
||||
}
|
||||
@@ -425,6 +701,13 @@ llama_model_deepseek2::graph::graph(const llama_model & model, const llm_graph_p
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@@ -114,7 +114,9 @@ void llama_model_deepseek4::load_arch_tensors(llama_model_loader & ml) {
|
||||
layer.wq_b = create_tensor(tn(LLM_TENSOR_ATTN_Q_B, "weight", i), {q_lora_rank, n_head * n_embd_head}, flags);
|
||||
layer.wkv = create_tensor(tn(LLM_TENSOR_ATTN_KV, "weight", i), {n_embd, n_embd_head}, flags);
|
||||
layer.attn_kv_norm = create_tensor(tn(LLM_TENSOR_ATTN_KV_NORM, "weight", i), {n_embd_head}, flags);
|
||||
layer.wo_a = create_tensor(tn(LLM_TENSOR_ATTN_OUT_A, "weight", i), {n_head * n_embd_head / o_groups, o_lora_rank * o_groups}, flags);
|
||||
// for wo_a, the shape in the file is (n_head * n_embd_head / o_groups, o_lora_rank*o_groups)
|
||||
// so we reshape here, to avoid reshaping the tensor in the graph
|
||||
layer.wo_a = create_tensor(tn(LLM_TENSOR_ATTN_OUT_A, "weight", i), {n_head * n_embd_head / o_groups, o_lora_rank, o_groups}, flags | TENSOR_ALLOW_RESHAPE);
|
||||
layer.wo_b = create_tensor(tn(LLM_TENSOR_ATTN_OUT_B, "weight", i), {o_groups * o_lora_rank, n_embd}, flags);
|
||||
|
||||
layer.hc_attn_fn = create_tensor(tn(LLM_TENSOR_HC_ATTN_FN, "weight", i), {hc_dim, hc_mix_dim}, flags);
|
||||
@@ -1258,7 +1260,7 @@ ggml_tensor * llama_model_deepseek4::graph::build_attention_impl(
|
||||
|
||||
out = ggml_reshape_3d(ctx0, out, o_group_dim, n_groups, nt);
|
||||
out = ggml_permute(ctx0, out, 0, 2, 1, 3);
|
||||
ggml_tensor * oa = ggml_mul_mat(ctx0, ggml_reshape_3d(ctx0, layer.wo_a, layer.wo_a->ne[0], o_lora_rank, n_groups), out);
|
||||
ggml_tensor * oa = ggml_mul_mat(ctx0, layer.wo_a, out);
|
||||
cb(oa, "attn_wo_a", il);
|
||||
oa = ggml_permute(ctx0, oa, 0, 2, 1, 3);
|
||||
oa = ggml_cont_2d(ctx0, oa, o_lora_rank*n_groups, nt);
|
||||
|
||||
@@ -1084,6 +1084,10 @@ struct llama_model_deepseek2 : 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<llm_graph_context> build_arch_graph(const llm_graph_params & params) const override;
|
||||
};
|
||||
|
||||
|
||||
@@ -258,6 +258,9 @@ llama_build_and_test(test-thread-safety.cpp ARGS -m "${MODEL_DEST}" -ngl 99 -p "
|
||||
set_tests_properties(test-thread-safety PROPERTIES FIXTURES_REQUIRED test-download-model)
|
||||
|
||||
llama_build_and_test(test-arg-parser.cpp)
|
||||
llama_build_and_test(test-model-resolution.cpp)
|
||||
# the test serves its repos from an httplib server, and the library links it privately
|
||||
target_link_libraries(test-model-resolution PRIVATE cpp-httplib)
|
||||
|
||||
if (NOT LLAMA_SANITIZE_ADDRESS AND NOT GGML_SCHED_NO_REALLOC)
|
||||
# TODO: repair known memory leaks
|
||||
|
||||
@@ -823,6 +823,7 @@ enum class penalties_position {
|
||||
static void add_filter_and_penalties(
|
||||
llama_sampler * chain,
|
||||
const sampler_init_fn & init_filter,
|
||||
int32_t n_vocab,
|
||||
int32_t penalty_last_n,
|
||||
float penalty_repeat,
|
||||
float penalty_freq,
|
||||
@@ -830,7 +831,7 @@ static void add_filter_and_penalties(
|
||||
penalties_position position) {
|
||||
const auto add_penalties = [&]() {
|
||||
llama_sampler_chain_add(chain, llama_sampler_init_penalties(
|
||||
penalty_last_n, penalty_repeat, penalty_freq, penalty_present));
|
||||
n_vocab, penalty_last_n, penalty_repeat, penalty_freq, penalty_present));
|
||||
};
|
||||
|
||||
if (position == penalties_position::before_filter) {
|
||||
@@ -1006,7 +1007,7 @@ static sampler_comparison_output run_penalties_comparison(
|
||||
const std::vector<float> raw_logits = decode_raw_logits(params, prompt);
|
||||
const auto add_samplers = [&](llama_sampler * chain) {
|
||||
llama_sampler_chain_add(chain, llama_sampler_init_penalties(
|
||||
penalty_last_n, penalty_repeat, penalty_freq, penalty_present));
|
||||
llama_vocab_n_tokens(vocab), penalty_last_n, penalty_repeat, penalty_freq, penalty_present));
|
||||
};
|
||||
const auto accept_history = [&](llama_sampler * chain) {
|
||||
accept_prompt(chain, vocab, prompt);
|
||||
@@ -1105,7 +1106,7 @@ static void compare_top_k_penalties_logits(
|
||||
GGML_ASSERT(excluded_history_token != LLAMA_TOKEN_NULL);
|
||||
|
||||
const auto add_samplers = [&](llama_sampler * chain) {
|
||||
add_filter_and_penalties(chain, init_top_k,
|
||||
add_filter_and_penalties(chain, init_top_k, n_vocab,
|
||||
penalty_last_n, penalty_repeat, penalty_freq, penalty_present, position);
|
||||
};
|
||||
|
||||
@@ -1190,7 +1191,7 @@ static void compare_masking_penalties_logits(
|
||||
GGML_ASSERT(masked_token != LLAMA_TOKEN_NULL);
|
||||
|
||||
const auto add_samplers = [&](llama_sampler * chain) {
|
||||
add_filter_and_penalties(chain, init_filter,
|
||||
add_filter_and_penalties(chain, init_filter, n_vocab,
|
||||
penalty_last_n, penalty_repeat, penalty_freq, penalty_present, position);
|
||||
};
|
||||
auto accept_history = [&](llama_sampler * smpl) {
|
||||
@@ -1218,7 +1219,7 @@ static void compare_masking_penalties_logits(
|
||||
GGML_ASSERT(fabsf(expected_logits.at(penalized_token) - raw_logits[penalized_token]) > 1e-6f);
|
||||
} else {
|
||||
llama_sampler_ptr penalties(llama_sampler_init_penalties(
|
||||
penalty_last_n, penalty_repeat, penalty_freq, penalty_present));
|
||||
n_vocab, penalty_last_n, penalty_repeat, penalty_freq, penalty_present));
|
||||
accept_history(penalties.get());
|
||||
const std::unordered_map<llama_token, float> penalized_logits =
|
||||
map_logits(apply_cpu_sampler(raw_logits, penalties.get()));
|
||||
|
||||
@@ -3987,6 +3987,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
|
||||
.expect_tool_calls({
|
||||
{ "special_function", R"({"arg1": 1})", {} },
|
||||
})
|
||||
.expect_reconstruction()
|
||||
.run();
|
||||
|
||||
// Tool call with negative number
|
||||
@@ -4212,6 +4213,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
|
||||
.expect_tool_calls({
|
||||
{ "special_function", R"({"arg1": 1})", {} },
|
||||
})
|
||||
.expect_reconstruction()
|
||||
.run();
|
||||
|
||||
// Tool call with multiple params (mixed types)
|
||||
@@ -4268,6 +4270,24 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
|
||||
.run();
|
||||
}
|
||||
|
||||
{
|
||||
// The DSML separator belongs to the tool call block, not assistant content.
|
||||
auto tst = peg_tester("models/templates/deepseek-ai-DeepSeek-V4-Flash-0731.jinja", detailed_debug);
|
||||
tst.test(
|
||||
"\n\n"
|
||||
"<|DSML|tool_calls>\n"
|
||||
"<|DSML|invoke name=\"special_function\">\n"
|
||||
"<|DSML|parameter name=\"arg1\" string=\"false\">1</|DSML|parameter>\n"
|
||||
"</|DSML|invoke>\n"
|
||||
"</|DSML|tool_calls>")
|
||||
.enable_thinking(false)
|
||||
.reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
|
||||
.tools({ special_function_tool })
|
||||
.expect(message_assist_call)
|
||||
.expect_reconstruction()
|
||||
.run();
|
||||
}
|
||||
|
||||
// GLM-4.6 tests - format: <tool_call>function_name\n<arg_key>...</arg_key>\n<arg_value>...</arg_value>\n</tool_call>
|
||||
{
|
||||
auto tst = peg_tester("models/templates/GLM-4.6.jinja", detailed_debug);
|
||||
@@ -6359,6 +6379,7 @@ static void test_template_generation_prompt() {
|
||||
std::vector<common_chat_msg> messages;
|
||||
bool add_generation_prompt = true;
|
||||
common_chat_continuation continue_final_message = COMMON_CHAT_CONTINUATION_NONE;
|
||||
bool enable_thinking = true;
|
||||
};
|
||||
|
||||
auto basic = [&]() {
|
||||
@@ -6390,6 +6411,7 @@ static void test_template_generation_prompt() {
|
||||
inputs.messages = opts.messages;
|
||||
inputs.add_generation_prompt = opts.add_generation_prompt;
|
||||
inputs.continue_final_message = opts.continue_final_message;
|
||||
inputs.enable_thinking = opts.enable_thinking;
|
||||
|
||||
auto params = common_chat_templates_apply(tmpls.get(), inputs);
|
||||
|
||||
@@ -6488,6 +6510,156 @@ static void test_template_generation_prompt() {
|
||||
check(tmpls, continuation_reasoning(), "<|Assistant|><think>I'm");
|
||||
}
|
||||
|
||||
const std::string deepseek_v4_reasoning_effort_max = "Reasoning Effort: Absolute maximum";
|
||||
const std::string deepseek_v4_flash_0731_reasoning_effort_max = "Reasoning Effort: Beyond maximum";
|
||||
|
||||
{
|
||||
auto tmpls = read_templates("models/templates/deepseek-ai-DeepSeek-V4.jinja");
|
||||
check(tmpls, basic(), "<|Assistant|><think>");
|
||||
check(tmpls, continuation_content(), "<|Assistant|><think>I'm thinking</think>Hello, ");
|
||||
check(tmpls, continuation_reasoning(), "<|Assistant|><think>I'm");
|
||||
|
||||
auto continuation_content_no_thinking = continuation_content();
|
||||
continuation_content_no_thinking.messages = { system_msg, message_user, simple_assist_msg("Hello, ") };
|
||||
continuation_content_no_thinking.enable_thinking = false;
|
||||
check(tmpls, continuation_content_no_thinking, "<|Assistant|></think>Hello, ");
|
||||
|
||||
common_chat_templates_inputs max_inputs;
|
||||
max_inputs.messages = { system_msg, message_user };
|
||||
max_inputs.chat_template_kwargs["reasoning_effort"] = R"("max")";
|
||||
auto max_params = common_chat_templates_apply(tmpls.get(), max_inputs);
|
||||
assert_contains(max_params.prompt, deepseek_v4_reasoning_effort_max);
|
||||
|
||||
auto high_inputs = max_inputs;
|
||||
high_inputs.chat_template_kwargs["reasoning_effort"] = R"("high")";
|
||||
auto high_params = common_chat_templates_apply(tmpls.get(), high_inputs);
|
||||
assert_not_contains(high_params.prompt, deepseek_v4_reasoning_effort_max);
|
||||
|
||||
auto low_inputs = max_inputs;
|
||||
low_inputs.chat_template_kwargs["reasoning_effort"] = R"("low")";
|
||||
auto low_params = common_chat_templates_apply(tmpls.get(), low_inputs);
|
||||
assert_not_contains(low_params.prompt, deepseek_v4_reasoning_effort_max);
|
||||
|
||||
common_chat_templates_inputs default_effort_inputs;
|
||||
default_effort_inputs.messages = { system_msg, message_user };
|
||||
auto default_effort_params = common_chat_templates_apply(tmpls.get(), default_effort_inputs);
|
||||
assert_not_contains(default_effort_params.prompt, deepseek_v4_reasoning_effort_max);
|
||||
|
||||
auto non_thinking_max_inputs = max_inputs;
|
||||
non_thinking_max_inputs.enable_thinking = false;
|
||||
auto non_thinking_max_params = common_chat_templates_apply(tmpls.get(), non_thinking_max_inputs);
|
||||
assert_not_contains(non_thinking_max_params.prompt, deepseek_v4_reasoning_effort_max);
|
||||
|
||||
common_chat_templates_inputs response_format_inputs;
|
||||
response_format_inputs.messages = { system_msg, message_user };
|
||||
response_format_inputs.tools = { get_time_tool };
|
||||
response_format_inputs.json_schema =
|
||||
R"({"type":"object","properties":{"answer":{"type":"string"}}})";
|
||||
auto response_format_params = common_chat_templates_apply(tmpls.get(), response_format_inputs);
|
||||
const auto tools_pos = response_format_params.prompt.find("## Tools");
|
||||
const auto response_format_pos = response_format_params.prompt.find(
|
||||
"## Response Format:\n\nYou MUST strictly adhere to the following schema to reply:\n");
|
||||
if (tools_pos == std::string::npos || response_format_pos == std::string::npos || tools_pos > response_format_pos) {
|
||||
LOG_ERR("Expected response format after tools\nActual: %s\n", response_format_params.prompt.c_str());
|
||||
common_log_flush(common_log_main());
|
||||
throw std::runtime_error("Test failed");
|
||||
}
|
||||
assert_contains(response_format_params.prompt, R"("answer": {"type": "string"})");
|
||||
|
||||
response_format_inputs.json_schema = "{}";
|
||||
auto json_object_params = common_chat_templates_apply(tmpls.get(), response_format_inputs);
|
||||
assert_contains(json_object_params.prompt,
|
||||
"## Response Format:\n\nYou MUST strictly adhere to the following schema to reply:\n{}");
|
||||
|
||||
common_chat_msg assistant_history;
|
||||
assistant_history.role = "assistant";
|
||||
assistant_history.content = "Previous answer";
|
||||
assistant_history.reasoning_content = "Previous reasoning";
|
||||
|
||||
common_chat_msg user_followup;
|
||||
user_followup.role = "user";
|
||||
user_followup.content = "Follow up";
|
||||
|
||||
common_chat_templates_inputs default_history_inputs;
|
||||
default_history_inputs.messages = { message_user, assistant_history, user_followup };
|
||||
auto default_history_params = common_chat_templates_apply(tmpls.get(), default_history_inputs);
|
||||
assert_contains(default_history_params.prompt, "<|Assistant|></think>Previous answer");
|
||||
|
||||
auto drop_thinking_inputs = default_history_inputs;
|
||||
drop_thinking_inputs.chat_template_kwargs["drop_thinking"] = "false";
|
||||
auto drop_thinking_params = common_chat_templates_apply(tmpls.get(), drop_thinking_inputs);
|
||||
assert_contains(drop_thinking_params.prompt, "<|Assistant|><think>Previous reasoning</think>Previous answer");
|
||||
|
||||
auto preserve_reasoning_inputs = default_history_inputs;
|
||||
preserve_reasoning_inputs.chat_template_kwargs["preserve_reasoning"] = "true";
|
||||
auto preserve_reasoning_params = common_chat_templates_apply(tmpls.get(), preserve_reasoning_inputs);
|
||||
assert_contains(preserve_reasoning_params.prompt, "<|Assistant|><think>Previous reasoning</think>Previous answer");
|
||||
assert_equals(true, common_chat_templates_get_caps(tmpls.get()).at("supports_preserve_reasoning"));
|
||||
|
||||
auto no_preserve_reasoning_inputs = default_history_inputs;
|
||||
no_preserve_reasoning_inputs.chat_template_kwargs["preserve_reasoning"] = "false";
|
||||
auto no_preserve_reasoning_params = common_chat_templates_apply(tmpls.get(), no_preserve_reasoning_inputs);
|
||||
assert_contains(no_preserve_reasoning_params.prompt, "<|Assistant|></think>Previous answer");
|
||||
|
||||
common_chat_msg empty_tool_call = simple_assist_msg("", "", "empty_args", "{}");
|
||||
common_chat_templates_inputs empty_tool_inputs;
|
||||
empty_tool_inputs.messages = { message_user, empty_tool_call };
|
||||
empty_tool_inputs.tools = { empty_args_tool };
|
||||
auto empty_tool_params = common_chat_templates_apply(tmpls.get(), empty_tool_inputs);
|
||||
assert_contains(empty_tool_params.prompt,
|
||||
"<|DSML|invoke name=\"empty_args\">\n\n</|DSML|invoke>");
|
||||
}
|
||||
|
||||
{
|
||||
auto tmpls = read_templates("models/templates/deepseek-ai-DeepSeek-V4-Flash-0731.jinja");
|
||||
check(tmpls, basic(), "<|Assistant|><think>");
|
||||
check(tmpls, continuation_content(), "<|Assistant|><think>I'm thinking</think>Hello, ");
|
||||
check(tmpls, continuation_reasoning(), "<|Assistant|><think>I'm");
|
||||
|
||||
auto continuation_content_no_thinking = continuation_content();
|
||||
continuation_content_no_thinking.messages = { system_msg, message_user, simple_assist_msg("Hello, ") };
|
||||
continuation_content_no_thinking.enable_thinking = false;
|
||||
check(tmpls, continuation_content_no_thinking, "<|Assistant|></think>Hello, ");
|
||||
|
||||
common_chat_templates_inputs high_inputs;
|
||||
high_inputs.messages = { system_msg, message_user };
|
||||
high_inputs.chat_template_kwargs["reasoning_effort"] = R"("high")";
|
||||
auto high_params = common_chat_templates_apply(tmpls.get(), high_inputs);
|
||||
assert_contains(high_params.prompt, deepseek_v4_reasoning_effort_max);
|
||||
|
||||
auto max_inputs = high_inputs;
|
||||
max_inputs.chat_template_kwargs["reasoning_effort"] = R"("max")";
|
||||
auto max_params = common_chat_templates_apply(tmpls.get(), max_inputs);
|
||||
assert_contains(max_params.prompt, deepseek_v4_flash_0731_reasoning_effort_max);
|
||||
|
||||
auto low_inputs = high_inputs;
|
||||
low_inputs.chat_template_kwargs["reasoning_effort"] = R"("low")";
|
||||
auto low_params = common_chat_templates_apply(tmpls.get(), low_inputs);
|
||||
assert_not_contains(low_params.prompt, deepseek_v4_reasoning_effort_max);
|
||||
assert_not_contains(low_params.prompt, deepseek_v4_flash_0731_reasoning_effort_max);
|
||||
|
||||
common_chat_templates_inputs default_effort_inputs;
|
||||
default_effort_inputs.messages = { system_msg, message_user };
|
||||
auto default_effort_params = common_chat_templates_apply(tmpls.get(), default_effort_inputs);
|
||||
assert_not_contains(default_effort_params.prompt, deepseek_v4_reasoning_effort_max);
|
||||
assert_not_contains(default_effort_params.prompt, deepseek_v4_flash_0731_reasoning_effort_max);
|
||||
|
||||
auto non_thinking_max_inputs = max_inputs;
|
||||
non_thinking_max_inputs.enable_thinking = false;
|
||||
auto non_thinking_max_params = common_chat_templates_apply(tmpls.get(), non_thinking_max_inputs);
|
||||
assert_not_contains(non_thinking_max_params.prompt, deepseek_v4_flash_0731_reasoning_effort_max);
|
||||
|
||||
common_chat_templates_inputs response_format_inputs;
|
||||
response_format_inputs.messages = { system_msg, message_user };
|
||||
response_format_inputs.tools = { get_time_tool };
|
||||
response_format_inputs.json_schema =
|
||||
R"({"type":"object","properties":{"answer":{"type":"string"}}})";
|
||||
auto response_format_params = common_chat_templates_apply(tmpls.get(), response_format_inputs);
|
||||
assert_contains(response_format_params.prompt,
|
||||
"## Response Format:\n\nYou MUST strictly adhere to the following schema to reply:\n");
|
||||
assert_contains(response_format_params.prompt, R"("answer": {"type": "string"})");
|
||||
}
|
||||
|
||||
{
|
||||
auto tmpls = read_templates("models/templates/openbmb-MiniCPM5-1B.jinja");
|
||||
check(tmpls, basic(), "<|im_start|>assistant\n<think>\n");
|
||||
|
||||
@@ -0,0 +1,506 @@
|
||||
// tests the HF model resolution and the model handler assembly end-to-end on
|
||||
// synthetic repo listings: a local httplib server bound to the loopback
|
||||
// serves hardcoded HF API responses, so the real client, hf_cache, resolution
|
||||
// and CLI parsing run against them without external network access
|
||||
|
||||
#include "arg.h"
|
||||
#include "common.h"
|
||||
#include "download.h"
|
||||
#include "http.h"
|
||||
#include "log.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <thread>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// the case and reordering being checked, printed with every failure
|
||||
static std::string g_context;
|
||||
|
||||
// independent of NDEBUG, so the checks stay alive in Release builds
|
||||
#define REQUIRE(x) do { \
|
||||
if (!(x)) { \
|
||||
fprintf(stderr, "%s:%d: [%s] REQUIRE(%s) failed\n", \
|
||||
__FILE__, __LINE__, g_context.c_str(), #x); \
|
||||
std::abort(); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define REQUIRE_EQ(actual, expected) do { \
|
||||
if (!((actual) == (expected))) { \
|
||||
fprintf(stderr, "%s:%d: [%s] REQUIRE_EQ(%s, %s) failed\n actual: '%s'\n expected: '%s'\n", \
|
||||
__FILE__, __LINE__, g_context.c_str(), #actual, #expected, \
|
||||
std::string(actual).c_str(), std::string(expected).c_str()); \
|
||||
std::abort(); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
//
|
||||
// synthetic repos keyed by repo id, served over the loopback by a real
|
||||
// httplib server, so the tested code runs its own client and transport
|
||||
//
|
||||
|
||||
static std::map<std::string, std::vector<std::string>> g_repos;
|
||||
|
||||
static const char * COMMIT = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
||||
|
||||
// the server lives in main, so its destructor runs before the static teardown
|
||||
// tears down the winsock state httplib brings in
|
||||
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(),
|
||||
"application/json");
|
||||
} else {
|
||||
res.status = 404;
|
||||
}
|
||||
});
|
||||
server.Get(R"(/api/models/(.+)/tree/.+)", [](const httplib::Request & req, httplib::Response & res) {
|
||||
if (!g_repos.count(req.matches[1])) {
|
||||
res.status = 404;
|
||||
return;
|
||||
}
|
||||
auto files = nlohmann::json::array();
|
||||
size_t i = 0;
|
||||
for (const auto & p : g_repos[req.matches[1]]) {
|
||||
char oid[41];
|
||||
snprintf(oid, sizeof(oid), "%040lx", (unsigned long) ++i);
|
||||
files.push_back({{"type", "file"}, {"path", p}, {"size", 1}, {"oid", oid}});
|
||||
}
|
||||
res.set_content(files.dump(), "application/json");
|
||||
});
|
||||
}
|
||||
|
||||
static common_params_model model_ref(const std::string & hf_repo, const std::string & hf_file = "") {
|
||||
common_params_model m;
|
||||
m.hf_repo = hf_repo;
|
||||
m.hf_file = hf_file;
|
||||
return m;
|
||||
}
|
||||
|
||||
// the model cache is isolated under a temporary directory named after the
|
||||
// loopback port, so concurrent runs on a shared machine keep their own, and
|
||||
// the local path the handler wires for a file is snapshots/<commit>/<path>
|
||||
static std::filesystem::path cache_dir;
|
||||
|
||||
static std::string cached(std::string repo_id, const std::string & path) {
|
||||
string_replace_all(repo_id, "/", "--");
|
||||
return (cache_dir / ("models--" + repo_id) / "snapshots" / COMMIT / path).string();
|
||||
}
|
||||
|
||||
//
|
||||
// fixtures mimicking real repo layouts
|
||||
//
|
||||
|
||||
// flat layout in the style of ggml-org/gemma-4-31B-it-GGUF
|
||||
static const std::vector<std::string> flat = {
|
||||
"README.md",
|
||||
"model-BF16.gguf",
|
||||
"model-Q4_K_M.gguf",
|
||||
"model-Q8_0.gguf",
|
||||
"mmproj-model-BF16.gguf",
|
||||
"mmproj-model-Q8_0.gguf",
|
||||
"mtp-model-BF16.gguf",
|
||||
"mtp-model-Q4_0.gguf",
|
||||
"mtp-model-Q8_0.gguf",
|
||||
"dflash-model-BF16.gguf",
|
||||
"dflash-model-Q8_0.gguf",
|
||||
};
|
||||
|
||||
// quants in subdirectories with sharded files and root sidecars,
|
||||
// in the style of stepfun-ai/Step-3.7-Flash-GGUF
|
||||
static const std::vector<std::string> subdir = {
|
||||
"mmproj-model-f16.gguf",
|
||||
"model-mtp-BF16.gguf",
|
||||
"model-mtp-Q8_0.gguf",
|
||||
"Q3_K_M/model-Q3_K_M-00001-of-00003.gguf",
|
||||
"Q3_K_M/model-Q3_K_M-00002-of-00003.gguf",
|
||||
"Q3_K_M/model-Q3_K_M-00003-of-00003.gguf",
|
||||
"Q8_0/model-Q8_0-00001-of-00002.gguf",
|
||||
"Q8_0/model-Q8_0-00002-of-00002.gguf",
|
||||
};
|
||||
|
||||
// sidecar quants exist where the full model quant does not,
|
||||
// in the style of ggml-org/Qwen3.6-27B-GGUF
|
||||
static const std::vector<std::string> hole = {
|
||||
"model-BF16.gguf",
|
||||
"model-Q4_K_M.gguf",
|
||||
"model-Q8_0.gguf",
|
||||
"mtp-model-BF16.gguf",
|
||||
"mtp-model-Q4_0.gguf",
|
||||
"mtp-model-Q8_0.gguf",
|
||||
"dflash-model-BF16.gguf",
|
||||
"dflash-model-Q8_0.gguf",
|
||||
};
|
||||
|
||||
// unsloth-style naming with UD quants and a suffix MTP file
|
||||
static const std::vector<std::string> unsloth = {
|
||||
"model-UD-Q8_K_XL.gguf",
|
||||
"mmproj-BF16.gguf",
|
||||
"model-MTP-BF16.gguf",
|
||||
};
|
||||
|
||||
// bartowski-style vendor prefix and mradermacher-style dot quant
|
||||
static const std::vector<std::string> vendors = {
|
||||
"TheDrummer_Model-24B-v4.1-Q8_0.gguf",
|
||||
"BlackSheep-24B.Q8_0.gguf",
|
||||
};
|
||||
|
||||
// every speculative sidecar type at the same quant
|
||||
static const std::vector<std::string> quad = {
|
||||
"model-Q8_0.gguf",
|
||||
"mtp-model-Q8_0.gguf",
|
||||
"dflash-model-Q8_0.gguf",
|
||||
"eagle3-model-Q8_0.gguf",
|
||||
"dspark-model-Q8_0.gguf",
|
||||
};
|
||||
|
||||
static const std::vector<std::string> dflash_only = {
|
||||
"model-Q8_0.gguf",
|
||||
"dflash-model-Q8_0.gguf",
|
||||
};
|
||||
|
||||
static const std::vector<std::string> eagle3_only = {
|
||||
"model-Q8_0.gguf",
|
||||
"eagle3-model-Q8_0.gguf",
|
||||
};
|
||||
|
||||
// a single full quant with dspark sidecars at other quants,
|
||||
// in the style of ggml-org/DeepSeek-V4-Flash-0731-GGUF
|
||||
static const std::vector<std::string> spark = {
|
||||
"README.md",
|
||||
"model-MXFP4.gguf",
|
||||
"dspark-model-BF16.gguf",
|
||||
"dspark-model-MXFP4.gguf",
|
||||
};
|
||||
|
||||
// dspark outranks dflash in the type auto-selection
|
||||
static const std::vector<std::string> dspark_dflash = {
|
||||
"model-Q8_0.gguf",
|
||||
"dflash-model-Q8_0.gguf",
|
||||
"dspark-model-Q8_0.gguf",
|
||||
};
|
||||
|
||||
//
|
||||
// table-driven plan resolution through the real entry point,
|
||||
// each case replayed on multiple deterministic reorderings of the listing,
|
||||
// except the cases whose pick legitimately depends on the listing order
|
||||
//
|
||||
|
||||
struct plan_case {
|
||||
const char * name;
|
||||
const std::vector<std::string> & files;
|
||||
const char * hf_repo;
|
||||
const char * hf_file;
|
||||
bool sidecars; // request mmproj + mtp + dflash + eagle3 + dspark
|
||||
bool order_dependent; // the expected pick depends on the listing order
|
||||
const char * primary;
|
||||
std::vector<std::string> model_files;
|
||||
const char * mmproj;
|
||||
const char * mtp;
|
||||
const char * dflash;
|
||||
const char * eagle3;
|
||||
const char * dspark;
|
||||
};
|
||||
|
||||
static const plan_case plan_cases[] = {
|
||||
// exact tag picks the matching primary, sidecars follow the tag
|
||||
{"flat exact tag", flat, "test/repo:Q8_0", "", true, false,
|
||||
"model-Q8_0.gguf", {"model-Q8_0.gguf"},
|
||||
"mmproj-model-Q8_0.gguf", "mtp-model-Q8_0.gguf", "dflash-model-Q8_0.gguf", "", ""},
|
||||
|
||||
// no tag falls back to the default quant preference
|
||||
{"flat default", flat, "test/repo", "", false, false,
|
||||
"model-Q4_K_M.gguf", {"model-Q4_K_M.gguf"},
|
||||
"", "", "", "", ""},
|
||||
|
||||
// no tag and no default match falls back to the first model in the listing
|
||||
{"unsloth fallback", unsloth, "test/repo", "", true, true,
|
||||
"model-UD-Q8_K_XL.gguf", {"model-UD-Q8_K_XL.gguf"},
|
||||
"mmproj-BF16.gguf", "", "", "", ""},
|
||||
|
||||
// explicit hf_file picks that exact file
|
||||
{"flat hf_file", flat, "test/repo", "model-BF16.gguf", false, false,
|
||||
"model-BF16.gguf", {"model-BF16.gguf"},
|
||||
"", "", "", "", ""},
|
||||
|
||||
// missing hf_file resolves nothing
|
||||
{"flat missing hf_file", flat, "test/repo", "nope.gguf", false, false,
|
||||
"", {},
|
||||
"", "", "", "", ""},
|
||||
|
||||
// a sharded primary brings all its parts, a subdir primary finds the root sidecar
|
||||
{"subdir shards", subdir, "test/repo:Q3_K_M", "", true, false,
|
||||
"Q3_K_M/model-Q3_K_M-00001-of-00003.gguf",
|
||||
{"Q3_K_M/model-Q3_K_M-00001-of-00003.gguf",
|
||||
"Q3_K_M/model-Q3_K_M-00002-of-00003.gguf",
|
||||
"Q3_K_M/model-Q3_K_M-00003-of-00003.gguf"},
|
||||
"mmproj-model-f16.gguf", "model-mtp-Q8_0.gguf", "", "", ""},
|
||||
|
||||
// a tag with no matching full model still resolves the requested sidecars
|
||||
{"hole tag sidecar", hole, "test/repo:Q4_0", "", true, false,
|
||||
"", {},
|
||||
"", "mtp-model-Q4_0.gguf", "dflash-model-Q8_0.gguf", "", ""},
|
||||
|
||||
// the same tag without a requested sidecar resolves nothing
|
||||
{"hole tag alone", hole, "test/repo:Q4_0", "", false, false,
|
||||
"", {},
|
||||
"", "", "", "", ""},
|
||||
|
||||
// no tag anchors the sidecars on the primary quant
|
||||
{"hole default anchor", hole, "test/repo", "", true, false,
|
||||
"model-Q4_K_M.gguf", {"model-Q4_K_M.gguf"},
|
||||
"", "mtp-model-Q4_0.gguf", "dflash-model-Q8_0.gguf", "", ""},
|
||||
|
||||
// the mtp- keyword is case sensitive, a suffix -MTP file is not discovered
|
||||
{"unsloth suffix mtp", unsloth, "test/repo:Q8_K_XL", "", true, false,
|
||||
"model-UD-Q8_K_XL.gguf", {"model-UD-Q8_K_XL.gguf"},
|
||||
"mmproj-BF16.gguf", "", "", "", ""},
|
||||
|
||||
// vendor prefixes and the dot quant convention both match the tag,
|
||||
// first match wins between two files at the same quant
|
||||
{"vendor prefix", vendors, "test/repo:Q8_0", "", false, true,
|
||||
"TheDrummer_Model-24B-v4.1-Q8_0.gguf", {"TheDrummer_Model-24B-v4.1-Q8_0.gguf"},
|
||||
"", "", "", "", ""},
|
||||
|
||||
// every sidecar type resolves at the tag
|
||||
{"quad exact tag", quad, "test/repo:Q8_0", "", true, false,
|
||||
"model-Q8_0.gguf", {"model-Q8_0.gguf"},
|
||||
"", "mtp-model-Q8_0.gguf", "dflash-model-Q8_0.gguf", "eagle3-model-Q8_0.gguf", "dspark-model-Q8_0.gguf"},
|
||||
|
||||
// no tag anchors the dspark sidecar on the only full quant
|
||||
{"spark default anchor", spark, "test/repo", "", true, false,
|
||||
"model-MXFP4.gguf", {"model-MXFP4.gguf"},
|
||||
"", "", "", "", "dspark-model-MXFP4.gguf"},
|
||||
|
||||
// a tag with no matching full model still resolves the exact dspark sidecar
|
||||
{"spark tag sidecar", spark, "test/repo:BF16", "", true, false,
|
||||
"", {},
|
||||
"", "", "", "", "dspark-model-BF16.gguf"},
|
||||
};
|
||||
|
||||
static void check_plan(const plan_case & c) {
|
||||
common_download_opts opts;
|
||||
opts.download_mmproj = c.sidecars;
|
||||
opts.download_mtp = c.sidecars;
|
||||
opts.download_dflash = c.sidecars;
|
||||
opts.download_eagle3 = c.sidecars;
|
||||
opts.download_dspark = c.sidecars;
|
||||
|
||||
auto plan = common_download_get_hf_plan(model_ref(c.hf_repo, c.hf_file), opts);
|
||||
|
||||
REQUIRE_EQ(plan.primary.path, c.primary);
|
||||
REQUIRE_EQ(plan.mmproj.path, c.mmproj);
|
||||
REQUIRE_EQ(plan.mtp.path, c.mtp);
|
||||
REQUIRE_EQ(plan.dflash.path, c.dflash);
|
||||
REQUIRE_EQ(plan.eagle3.path, c.eagle3);
|
||||
REQUIRE_EQ(plan.dspark.path, c.dspark);
|
||||
|
||||
// exact shard set, order insensitive; the primary must be the first split
|
||||
std::vector<std::string> actual;
|
||||
for (const auto & f : plan.model_files) {
|
||||
actual.push_back(f.path);
|
||||
}
|
||||
std::sort(actual.begin(), actual.end());
|
||||
auto expected = c.model_files;
|
||||
std::sort(expected.begin(), expected.end());
|
||||
REQUIRE(actual == expected);
|
||||
if (!expected.empty()) {
|
||||
REQUIRE(plan.primary.path == expected.front());
|
||||
}
|
||||
}
|
||||
|
||||
static void test_plan_resolution() {
|
||||
printf("test-model-resolution: plan resolution on %zu cases\n", sizeof(plan_cases) / sizeof(plan_cases[0]));
|
||||
|
||||
for (const auto & c : plan_cases) {
|
||||
printf(" %s\n", c.name);
|
||||
// invariant: the resolution is insensitive to the listing order
|
||||
for (size_t rot = 0; rot < c.files.size(); ++rot) {
|
||||
if (c.order_dependent && rot > 0) {
|
||||
continue;
|
||||
}
|
||||
g_context = std::string(c.name) + ", reordering " + std::to_string(rot);
|
||||
auto files = c.files;
|
||||
std::rotate(files.begin(), files.begin() + rot, files.end());
|
||||
if (rot % 2 == 1) {
|
||||
std::reverse(files.begin(), files.end());
|
||||
}
|
||||
g_repos["test/repo"] = files;
|
||||
check_plan(c);
|
||||
}
|
||||
}
|
||||
g_repos.clear();
|
||||
}
|
||||
|
||||
//
|
||||
// end-to-end assembly: real CLI parsing, real handler init resolving over the
|
||||
// loopback, downloads skipped by flipping offline before apply
|
||||
//
|
||||
|
||||
static void assemble(std::vector<std::string> argv, common_params & params) {
|
||||
std::vector<char *> cargv;
|
||||
g_context.clear();
|
||||
for (auto & a : argv) {
|
||||
g_context += g_context.empty() ? a : " " + a;
|
||||
cargv.push_back(a.data());
|
||||
}
|
||||
bool ok = common_params_parse((int) cargv.size(), cargv.data(), params, LLAMA_EXAMPLE_SERVER);
|
||||
REQUIRE(ok);
|
||||
|
||||
auto handler = common_models_handler_init(params, LLAMA_EXAMPLE_SERVER);
|
||||
|
||||
// skip the network execution, on_done still wires the params
|
||||
params.offline = true;
|
||||
common_models_handler_apply(handler, params);
|
||||
}
|
||||
|
||||
static void test_task_assembly() {
|
||||
printf("test-model-resolution: end-to-end assembly\n");
|
||||
|
||||
g_repos["test/main"] = flat;
|
||||
g_repos["test/hole"] = hole;
|
||||
g_repos["test/quad"] = quad;
|
||||
g_repos["test/dflash"] = dflash_only;
|
||||
g_repos["test/eagle3"] = eagle3_only;
|
||||
g_repos["test/spark"] = spark;
|
||||
g_repos["test/pair"] = dspark_dflash;
|
||||
g_repos["test/small"] = {"draft-model-Q4_K_M.gguf"};
|
||||
g_repos["test/preset"] = {"preset.ini", "model-Q8_0.gguf"};
|
||||
|
||||
{
|
||||
// plain -hf wires the model and its mmproj, nothing speculative
|
||||
common_params params;
|
||||
assemble({"server", "-hf", "test/main:Q8_0"}, params);
|
||||
REQUIRE_EQ(params.model.path, cached("test/main", "model-Q8_0.gguf"));
|
||||
REQUIRE_EQ(params.mmproj.path, cached("test/main", "mmproj-model-Q8_0.gguf"));
|
||||
REQUIRE(params.speculative.draft.mparams.path.empty());
|
||||
}
|
||||
{
|
||||
// --no-mmproj disables the mmproj discovery
|
||||
common_params params;
|
||||
assemble({"server", "-hf", "test/main:Q8_0", "--no-mmproj"}, params);
|
||||
REQUIRE(params.mmproj.path.empty());
|
||||
}
|
||||
{
|
||||
// an explicit --mmproj wins over the discovery
|
||||
common_params params;
|
||||
assemble({"server", "-hf", "test/main:Q8_0", "--mmproj", "/local/mmproj.gguf"}, params);
|
||||
REQUIRE(params.mmproj.path == "/local/mmproj.gguf");
|
||||
}
|
||||
{
|
||||
// -hf with a spec type wires the sidecar of the main repo as fallback draft
|
||||
common_params params;
|
||||
assemble({"server", "-hf", "test/main:Q8_0", "--spec-type", "draft-mtp"}, params);
|
||||
REQUIRE_EQ(params.speculative.draft.mparams.path, cached("test/main", "mtp-model-Q8_0.gguf"));
|
||||
}
|
||||
{
|
||||
// -hfd with a spec type wires the draft repo sidecar at its tag,
|
||||
// not its full model, and suppresses the main repo fallback
|
||||
common_params params;
|
||||
assemble({"server", "-hf", "test/hole:Q8_0", "-hfd", "test/hole:Q4_0", "--spec-type", "draft-mtp"}, params);
|
||||
REQUIRE_EQ(params.speculative.draft.mparams.path, cached("test/hole", "mtp-model-Q4_0.gguf"));
|
||||
}
|
||||
{
|
||||
// an explicit -md file wins over the sidecar resolution
|
||||
common_params params;
|
||||
assemble({"server", "-hf", "test/main:Q8_0", "-hfd", "test/main", "-md", "mtp-model-BF16.gguf", "--spec-type", "draft-mtp"}, params);
|
||||
REQUIRE_EQ(params.speculative.draft.mparams.path, cached("test/main", "mtp-model-BF16.gguf"));
|
||||
}
|
||||
{
|
||||
// -hfd without a spec type auto-selects the type, mtp first when all ship
|
||||
common_params params;
|
||||
assemble({"server", "-hf", "test/main:Q8_0", "-hfd", "test/quad:Q8_0"}, params);
|
||||
REQUIRE(params.speculative.types == std::vector<enum common_speculative_type>{COMMON_SPECULATIVE_TYPE_DRAFT_MTP});
|
||||
REQUIRE_EQ(params.speculative.draft.mparams.path, cached("test/quad", "mtp-model-Q8_0.gguf"));
|
||||
}
|
||||
{
|
||||
// auto-selection with only a dflash sidecar
|
||||
common_params params;
|
||||
assemble({"server", "-hf", "test/main:Q8_0", "-hfd", "test/dflash:Q8_0"}, params);
|
||||
REQUIRE(params.speculative.types == std::vector<enum common_speculative_type>{COMMON_SPECULATIVE_TYPE_DRAFT_DFLASH});
|
||||
REQUIRE_EQ(params.speculative.draft.mparams.path, cached("test/dflash", "dflash-model-Q8_0.gguf"));
|
||||
}
|
||||
{
|
||||
// auto-selection with only an eagle3 sidecar
|
||||
common_params params;
|
||||
assemble({"server", "-hf", "test/main:Q8_0", "-hfd", "test/eagle3:Q8_0"}, params);
|
||||
REQUIRE(params.speculative.types == std::vector<enum common_speculative_type>{COMMON_SPECULATIVE_TYPE_DRAFT_EAGLE3});
|
||||
REQUIRE_EQ(params.speculative.draft.mparams.path, cached("test/eagle3", "eagle3-model-Q8_0.gguf"));
|
||||
}
|
||||
{
|
||||
// auto-selection prefers dspark over dflash when both ship
|
||||
common_params params;
|
||||
assemble({"server", "-hf", "test/main:Q8_0", "-hfd", "test/pair:Q8_0"}, params);
|
||||
REQUIRE(params.speculative.types == std::vector<enum common_speculative_type>{COMMON_SPECULATIVE_TYPE_DRAFT_DSPARK});
|
||||
REQUIRE_EQ(params.speculative.draft.mparams.path, cached("test/pair", "dspark-model-Q8_0.gguf"));
|
||||
}
|
||||
{
|
||||
// -hf with the dspark spec type wires the sidecar of the main repo,
|
||||
// anchored on the only full quant
|
||||
common_params params;
|
||||
assemble({"server", "-hf", "test/spark", "--spec-type", "draft-dspark"}, params);
|
||||
REQUIRE_EQ(params.model.path, cached("test/spark", "model-MXFP4.gguf"));
|
||||
REQUIRE_EQ(params.speculative.draft.mparams.path, cached("test/spark", "dspark-model-MXFP4.gguf"));
|
||||
}
|
||||
{
|
||||
// -hfd on a repo without sidecars keeps resolving a full model as draft
|
||||
common_params params;
|
||||
assemble({"server", "-hf", "test/main:Q8_0", "-hfd", "test/small"}, params);
|
||||
REQUIRE(params.speculative.types == std::vector<enum common_speculative_type>{COMMON_SPECULATIVE_TYPE_NONE});
|
||||
REQUIRE_EQ(params.speculative.draft.mparams.path, cached("test/small", "draft-model-Q4_K_M.gguf"));
|
||||
}
|
||||
{
|
||||
// a preset repo wires the preset and clears the model for router mode
|
||||
common_params params;
|
||||
assemble({"server", "-hf", "test/preset"}, params);
|
||||
REQUIRE_EQ(params.models_preset, cached("test/preset", "preset.ini"));
|
||||
REQUIRE(params.model.path.empty());
|
||||
REQUIRE(params.model.hf_repo.empty());
|
||||
}
|
||||
|
||||
g_repos.clear();
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
// unbuffered, so a crash cannot swallow the reports already printed
|
||||
setvbuf(stdout, nullptr, _IONBF, 0);
|
||||
setvbuf(stderr, nullptr, _IONBF, 0);
|
||||
|
||||
// the negative cases legitimately log errors on every reordering,
|
||||
// keep the output down to the reports
|
||||
common_log_pause(common_log_main());
|
||||
|
||||
// the loopback endpoint also keeps the client init from rejecting
|
||||
// https on the builds without TLS support
|
||||
httplib::Server server;
|
||||
serve_repos(server);
|
||||
int port = server.bind_to_any_port("127.0.0.1");
|
||||
|
||||
// isolate the cache, its location is read once so it is set
|
||||
// before anything else
|
||||
cache_dir = std::filesystem::temp_directory_path() /
|
||||
("test-model-resolution-cache-" + std::to_string(port));
|
||||
std::filesystem::remove_all(cache_dir);
|
||||
common_set_env("LLAMA_CACHE", cache_dir.string());
|
||||
|
||||
std::thread server_thread([&server] { server.listen_after_bind(); });
|
||||
server.wait_until_ready();
|
||||
common_set_env("MODEL_ENDPOINT", "http://127.0.0.1:" + std::to_string(port) + "/");
|
||||
|
||||
test_plan_resolution();
|
||||
test_task_assembly();
|
||||
|
||||
server.stop();
|
||||
server_thread.join();
|
||||
|
||||
std::filesystem::remove_all(cache_dir);
|
||||
printf("test-model-resolution: all tests OK\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -144,7 +144,7 @@ static void test_penalties(
|
||||
|
||||
sampler_tester tester(probs, probs_expected);
|
||||
|
||||
auto * sampler = llama_sampler_init_penalties(last_tokens.size(), repeat_penalty, alpha_frequency, alpha_presence);
|
||||
auto * sampler = llama_sampler_init_penalties((int32_t) probs.size(), (int32_t) last_tokens.size(), repeat_penalty, alpha_frequency, alpha_presence);
|
||||
|
||||
for (size_t i = 0; i < last_tokens.size(); i++) {
|
||||
llama_sampler_accept(sampler, last_tokens[i]);
|
||||
|
||||
@@ -198,7 +198,9 @@ For the full list of features, please refer to [server's changelog](https://gith
|
||||
| `--ui-config, --webui-config JSON` | JSON that provides default UI settings (overrides UI defaults)<br/>(env: LLAMA_ARG_UI_CONFIG) |
|
||||
| `--ui-config-file, --webui-config-file PATH` | JSON file that provides default UI settings (overrides UI defaults)<br/>(env: LLAMA_ARG_UI_CONFIG_FILE) |
|
||||
| `--ui-mcp-proxy, --webui-mcp-proxy, --no-ui-mcp-proxy, --no-webui-mcp-proxy` | experimental: whether to enable MCP CORS proxy - do not enable in untrusted environments (default: disabled)<br/>(env: LLAMA_ARG_UI_MCP_PROXY) |
|
||||
| `--tools TOOL1,TOOL2,...` | experimental: whether to enable built-in tools for AI agents - do not enable in untrusted environments (default: no tools)<br/>specify "all" to enable all tools<br/>available tools: read_file, file_glob_search, grep_search, exec_shell_command, write_file, edit_file, get_datetime<br/>note: for security reasons, this will limit --cors-origins to localhost by default<br/>(env: LLAMA_ARG_TOOLS) |
|
||||
| `--tools TOOL1,TOOL2,...` | experimental: whether to enable built-in tools for AI agents - do not enable in untrusted environments (default: no tools)<br/>specify "all" to enable all tools<br/>available tools: read_file, file_glob_search, grep_search, exec_shell_command, write_file, edit_file, get_datetime, get_info<br/>note: for security reasons, this will limit --cors-origins to localhost by default<br/>(env: LLAMA_ARG_TOOLS) |
|
||||
| `--mcp-servers-config PATH` | experimental: path to JSON file with MCP server definitions (Cursor-compatible format) - do not enable in untrusted environments (default: none)<br/>note: for security reasons, this will limit --cors-origins to localhost by default<br/>(env: LLAMA_ARG_MCP_SERVERS_CONFIG) |
|
||||
| `--mcp-servers-json JSON` | experimental: inline JSON with MCP server definitions (Cursor-compatible format) - do not enable in untrusted environments (default: none)<br/>note: for security reasons, this will limit --cors-origins to localhost by default<br/>(env: LLAMA_ARG_MCP_SERVERS_JSON) |
|
||||
| `-ag, --agent, -no-ag, --no-agent` | whether to enable CORS proxy and all built-in tools - do not enable in untrusted environments (default: disabled)<br/>note: for security reasons, this will limit --cors-origins to localhost by default<br/>(env: LLAMA_ARG_AGENT) |
|
||||
| `--ui, --webui, --no-ui, --no-webui` | whether to enable the Web UI (default: enabled)<br/>(env: LLAMA_ARG_UI) |
|
||||
| `--embedding, --embeddings` | restrict to only support embedding use case; use only with dedicated embedding models (default: disabled)<br/>(env: LLAMA_ARG_EMBEDDINGS) |
|
||||
|
||||
@@ -1090,6 +1090,56 @@ struct server_tool_get_datetime : server_tool {
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// get_info: returns runtime info (OS name/version and cwd)
|
||||
//
|
||||
|
||||
struct server_tool_get_info : server_tool {
|
||||
server_tool_get_info() {
|
||||
name = "get_info";
|
||||
display_name = "Get Runtime Info";
|
||||
permission_write = false;
|
||||
}
|
||||
|
||||
json get_definition() const override {
|
||||
return {
|
||||
{"type", "function"},
|
||||
{"function", {
|
||||
{"name", name},
|
||||
{"description", "Returns runtime info: the OS name/version and the current working directory"},
|
||||
{"parameters", {
|
||||
{"type", "object"},
|
||||
{"properties", json::object()},
|
||||
}},
|
||||
}},
|
||||
};
|
||||
}
|
||||
|
||||
json invoke(json params, server_tool::stream *) const override {
|
||||
auto io = make_tools_io(params);
|
||||
|
||||
#ifdef _WIN32
|
||||
auto res = io->run({"cmd", "/c", "ver"}, 4096, 5);
|
||||
#else
|
||||
auto res = io->run({"uname", "-a"}, 4096, 5);
|
||||
#endif
|
||||
// "ver" prints a blank line before the version, so the output is stripped on both ends;
|
||||
// a failed spawn or a timeout leaves a diagnostic in res.output, which is not an OS name
|
||||
std::string os_info = res.exit_code == 0 && !res.timed_out ? string_strip(res.output) : "unknown";
|
||||
|
||||
std::string cwd = json_value(params, "cwd", std::string());
|
||||
if (cwd.empty()) {
|
||||
std::error_code ec;
|
||||
cwd = fs::current_path(ec).string();
|
||||
}
|
||||
|
||||
return {
|
||||
{"os", os_info},
|
||||
{"cwd", cwd},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
struct server_tool_stream_result : server_task_result {
|
||||
std::string chunk;
|
||||
bool done = false;
|
||||
@@ -1199,6 +1249,7 @@ static std::vector<std::unique_ptr<server_tool>> build_tools() {
|
||||
tools.push_back(std::make_unique<server_tool_write_file>());
|
||||
tools.push_back(std::make_unique<server_tool_edit_file>());
|
||||
tools.push_back(std::make_unique<server_tool_get_datetime>());
|
||||
tools.push_back(std::make_unique<server_tool_get_info>());
|
||||
return tools;
|
||||
}
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -41,7 +41,7 @@ if (LLAMA_BUILD_BORINGSSL)
|
||||
set(FIPS OFF CACHE BOOL "Enable FIPS (BoringSSL)")
|
||||
|
||||
set(BORINGSSL_GIT "https://boringssl.googlesource.com/boringssl" CACHE STRING "BoringSSL git repository")
|
||||
set(BORINGSSL_VERSION "0.20260730.0" CACHE STRING "BoringSSL version")
|
||||
set(BORINGSSL_VERSION "0.20260803.0" CACHE STRING "BoringSSL version")
|
||||
|
||||
message(STATUS "Fetching BoringSSL version ${BORINGSSL_VERSION}")
|
||||
|
||||
|
||||
Vendored
+411
-174
@@ -1412,6 +1412,46 @@ bool stream_line_reader::getline() {
|
||||
#endif
|
||||
|
||||
for (size_t i = 0;; i++) {
|
||||
// Fast path: whatever the stream has already buffered can be scanned for
|
||||
// the terminator in one pass. Asking for a byte at a time costs a virtual
|
||||
// call, a bounds check and a one-byte copy per character of the request.
|
||||
size_t buffered_size = 0;
|
||||
if (auto buffered = strm_.buffered_data(buffered_size)) {
|
||||
auto take = buffered_size;
|
||||
auto terminated = false;
|
||||
|
||||
for (size_t at = 0; at < buffered_size;) {
|
||||
auto nl = static_cast<const char *>(
|
||||
memchr(buffered + at, '\n', buffered_size - at));
|
||||
if (!nl) { break; }
|
||||
auto pos = static_cast<size_t>(nl - buffered);
|
||||
#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR
|
||||
take = pos + 1;
|
||||
terminated = true;
|
||||
break;
|
||||
#else
|
||||
// A bare LF does not end the line; keep looking for CRLF. The CR may
|
||||
// be the last byte of an earlier chunk, hence prev_byte.
|
||||
if ((pos > 0 ? buffered[pos - 1] : prev_byte) == '\r') {
|
||||
take = pos + 1;
|
||||
terminated = true;
|
||||
break;
|
||||
}
|
||||
at = pos + 1;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (size() + take > CPPHTTPLIB_MAX_LINE_LENGTH) { return false; }
|
||||
#ifndef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR
|
||||
prev_byte = buffered[take - 1];
|
||||
#endif
|
||||
append(buffered, take);
|
||||
strm_.consume_buffered(take);
|
||||
i += take;
|
||||
if (terminated) { return true; }
|
||||
continue;
|
||||
}
|
||||
|
||||
if (size() >= CPPHTTPLIB_MAX_LINE_LENGTH) {
|
||||
// Treat exceptionally long lines as an error to
|
||||
// prevent infinite loops/memory exhaustion
|
||||
@@ -1443,16 +1483,26 @@ bool stream_line_reader::getline() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void stream_line_reader::append(char c) {
|
||||
if (fixed_buffer_used_size_ < fixed_buffer_size_ - 1) {
|
||||
fixed_buffer_[fixed_buffer_used_size_++] = c;
|
||||
void stream_line_reader::append(char c) { append(&c, 1); }
|
||||
|
||||
void stream_line_reader::append(const char *data, size_t size) {
|
||||
// Once the line has outgrown the fixed buffer everything must keep going to
|
||||
// the growable one, even if a later chunk would have fit. Without the
|
||||
// emptiness check a short append after a long one would land in the fixed
|
||||
// buffer, which ptr() and size() no longer look at, and be lost.
|
||||
if (growable_buffer_.empty() &&
|
||||
fixed_buffer_used_size_ + size < fixed_buffer_size_) {
|
||||
memcpy(fixed_buffer_ + fixed_buffer_used_size_, data, size);
|
||||
fixed_buffer_used_size_ += size;
|
||||
fixed_buffer_[fixed_buffer_used_size_] = '\0';
|
||||
} else {
|
||||
// Unlike the per-character overload, this can be the very first append of
|
||||
// the line, so the fixed buffer may hold nothing and carry no terminator
|
||||
// yet. assign() takes an explicit length and does not need one.
|
||||
if (growable_buffer_.empty()) {
|
||||
assert(fixed_buffer_[fixed_buffer_used_size_] == '\0');
|
||||
growable_buffer_.assign(fixed_buffer_, fixed_buffer_used_size_);
|
||||
}
|
||||
growable_buffer_ += c;
|
||||
growable_buffer_.append(data, size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1525,6 +1575,14 @@ bool mmap::open(const char *path) {
|
||||
is_open_empty_file = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (addr_ == MAP_FAILED) {
|
||||
// Clear the sentinel before `close()`, since `is_open()` only checks
|
||||
// `addr_` against nullptr and `munmap()` must not be called with it.
|
||||
addr_ = nullptr;
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
return true;
|
||||
@@ -1702,8 +1760,17 @@ public:
|
||||
socket_t socket() const override;
|
||||
time_t duration() const override;
|
||||
void set_read_timeout(time_t sec, time_t usec = 0) override;
|
||||
const char *buffered_data(size_t &size) const override;
|
||||
void consume_buffered(size_t size) override;
|
||||
|
||||
// The caller has just seen this socket become readable. Lets the next read
|
||||
// skip its own readiness wait, which would otherwise ask the kernel a
|
||||
// question that was answered a moment ago. Consumed by that read.
|
||||
void set_readable_hint() { readable_hint_ = true; }
|
||||
|
||||
private:
|
||||
bool ensure_readable();
|
||||
|
||||
socket_t sock_;
|
||||
time_t read_timeout_sec_;
|
||||
time_t read_timeout_usec_;
|
||||
@@ -1715,6 +1782,7 @@ private:
|
||||
std::vector<char> read_buff_;
|
||||
size_t read_buff_off_ = 0;
|
||||
size_t read_buff_content_size_ = 0;
|
||||
bool readable_hint_ = false;
|
||||
|
||||
static const size_t read_buff_size_ = 1024l * 4;
|
||||
};
|
||||
@@ -1782,6 +1850,9 @@ process_server_socket(const std::atomic<socket_t> &svr_sock, socket_t sock,
|
||||
[&](bool close_connection, bool &connection_closed) {
|
||||
SocketStream strm(sock, read_timeout_sec, read_timeout_usec,
|
||||
write_timeout_sec, write_timeout_usec);
|
||||
// process_server_socket_core() only gets here once keep_alive() has
|
||||
// seen the socket go readable.
|
||||
strm.set_readable_hint();
|
||||
return callback(strm, close_connection, connection_closed);
|
||||
});
|
||||
}
|
||||
@@ -3071,19 +3142,49 @@ bool zstd_decompressor::decompress(const char *data, size_t data_length,
|
||||
}
|
||||
#endif
|
||||
|
||||
bool contains_case_ignore(const std::string &s, const char *token) {
|
||||
auto token_end = token + std::strlen(token);
|
||||
return std::search(s.begin(), s.end(), token, token_end, [](char a, char b) {
|
||||
return case_ignore::to_lower(a) == case_ignore::to_lower(b);
|
||||
}) != s.end();
|
||||
}
|
||||
|
||||
// Content codings are case-insensitive (RFC 9110 8.4.1). Matching them
|
||||
// case-sensitively would make a response labeled e.g. "GZIP" look like an
|
||||
// unknown coding, and its payload would be handed back still compressed.
|
||||
bool is_zlib_encoding(const std::string &encoding) {
|
||||
return case_ignore::equal(encoding, "gzip") ||
|
||||
case_ignore::equal(encoding, "deflate");
|
||||
}
|
||||
|
||||
bool is_brotli_encoding(const std::string &encoding) {
|
||||
return contains_case_ignore(encoding, "br");
|
||||
}
|
||||
|
||||
bool is_zstd_encoding(const std::string &encoding) {
|
||||
return contains_case_ignore(encoding, "zstd");
|
||||
}
|
||||
|
||||
// Returns true if the content coding is one cpp-httplib is able to decompress
|
||||
// when the corresponding support is compiled in.
|
||||
bool is_known_content_encoding(const std::string &encoding) {
|
||||
return is_zlib_encoding(encoding) || is_brotli_encoding(encoding) ||
|
||||
is_zstd_encoding(encoding);
|
||||
}
|
||||
|
||||
std::unique_ptr<decompressor>
|
||||
create_decompressor(const std::string &encoding) {
|
||||
std::unique_ptr<decompressor> decompressor;
|
||||
|
||||
if (encoding == "gzip" || encoding == "deflate") {
|
||||
if (is_zlib_encoding(encoding)) {
|
||||
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
|
||||
decompressor = detail::make_unique<gzip_decompressor>();
|
||||
#endif
|
||||
} else if (encoding.find("br") != std::string::npos) {
|
||||
} else if (is_brotli_encoding(encoding)) {
|
||||
#ifdef CPPHTTPLIB_BROTLI_SUPPORT
|
||||
decompressor = detail::make_unique<brotli_decompressor>();
|
||||
#endif
|
||||
} else if (encoding == "zstd" || encoding.find("zstd") != std::string::npos) {
|
||||
} else if (is_zstd_encoding(encoding)) {
|
||||
#ifdef CPPHTTPLIB_ZSTD_SUPPORT
|
||||
decompressor = detail::make_unique<zstd_decompressor>();
|
||||
#endif
|
||||
@@ -3145,8 +3246,7 @@ const char *get_header_value(const Headers &headers,
|
||||
|
||||
size_t get_header_value_count(const Headers &headers,
|
||||
const std::string &key) {
|
||||
auto r = headers.equal_range(key);
|
||||
return static_cast<size_t>(std::distance(r.first, r.second));
|
||||
return headers.count(key);
|
||||
}
|
||||
|
||||
template <typename Map>
|
||||
@@ -3370,44 +3470,33 @@ ReadContentResult read_content_chunked(Stream &strm, T &x,
|
||||
bool is_chunked_transfer_encoding(const Headers &headers) {
|
||||
// RFC 9112 6.1: a message is framed with the chunked coding when "chunked"
|
||||
// is the final transfer coding. A single field value may list several
|
||||
// codings ("gzip, chunked"), and the list may be split across multiple
|
||||
// Transfer-Encoding header lines (RFC 9110 5.3). Match the last coding token
|
||||
// case-insensitively rather than comparing the whole value against "chunked".
|
||||
// codings ("gzip, chunked"), and RFC 9110 5.3 lets that list be split across
|
||||
// several Transfer-Encoding lines, which combine into one comma-separated
|
||||
// list in the order the lines were received. Headers preserves that order,
|
||||
// so the final coding is the last token of the last line. Match it
|
||||
// case-insensitively rather than comparing the whole value against
|
||||
// "chunked".
|
||||
//
|
||||
// Security: reading a chunked message as unframed leaves its body in the
|
||||
// socket, where a keep-alive connection parses it as a smuggled request.
|
||||
// Headers is an unordered_multimap whose iteration order for duplicate keys
|
||||
// is not portable, so when there is more than one Transfer-Encoding line we
|
||||
// cannot tell which coding is truly final. In that ambiguous case we fail
|
||||
// safe by treating the message as chunked (a mis-parse just closes the
|
||||
// connection, whereas the opposite error enables smuggling).
|
||||
// Server::process_request() answers 400 and closes when the final coding is
|
||||
// not chunked, so a request whose framing cannot be determined never
|
||||
// reaches the "no body" path.
|
||||
auto rng = headers.equal_range("Transfer-Encoding");
|
||||
if (rng.first == rng.second) { return false; }
|
||||
|
||||
size_t line_count = 0;
|
||||
bool chunked_present = false;
|
||||
bool last_line_ends_with_chunked = false;
|
||||
// Cleared per line, so a trailing line carrying no coding at all leaves the
|
||||
// combined list ending in nothing rather than inheriting the line before it.
|
||||
std::string last_coding;
|
||||
|
||||
for (auto it = rng.first; it != rng.second; ++it) {
|
||||
line_count++;
|
||||
const auto &value = it->second;
|
||||
|
||||
std::string last_coding;
|
||||
bool line_has_chunked = false;
|
||||
last_coding.clear();
|
||||
split(value.data(), value.data() + value.size(), ',',
|
||||
[&](const char *b, const char *e) {
|
||||
last_coding.assign(b, e);
|
||||
if (case_ignore::equal(last_coding, "chunked")) {
|
||||
line_has_chunked = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (line_has_chunked) { chunked_present = true; }
|
||||
last_line_ends_with_chunked = case_ignore::equal(last_coding, "chunked");
|
||||
[&](const char *b, const char *e) { last_coding.assign(b, e); });
|
||||
}
|
||||
|
||||
if (line_count == 0) { return false; }
|
||||
if (line_count == 1) { return last_line_ends_with_chunked; }
|
||||
return chunked_present;
|
||||
return case_ignore::equal(last_coding, "chunked");
|
||||
}
|
||||
|
||||
template <typename T, typename U>
|
||||
@@ -3420,9 +3509,12 @@ bool prepare_content_receiver(T &x, int &status,
|
||||
std::unique_ptr<decompressor> decompressor;
|
||||
|
||||
if (!encoding.empty()) {
|
||||
// A coding we know about but were not built with is an error. An
|
||||
// unrecognized coding (including "identity") is left alone and the
|
||||
// payload is passed through as-is, since some servers misuse the header,
|
||||
// e.g. by sending a character set such as "Content-Encoding: UTF-8".
|
||||
decompressor = detail::create_decompressor(encoding);
|
||||
if (!decompressor) {
|
||||
// Unsupported encoding or no support compiled in
|
||||
if (!decompressor && detail::is_known_content_encoding(encoding)) {
|
||||
status = StatusCode::UnsupportedMediaType_415;
|
||||
return false;
|
||||
}
|
||||
@@ -3845,6 +3937,19 @@ std::string params_to_query_str(const Params ¶ms) {
|
||||
return query;
|
||||
}
|
||||
|
||||
// Splits one "key=value" span of a query string at its first '='. A span with
|
||||
// no '=' at all lands entirely in key, leaving val empty, which is how a bare
|
||||
// "?flag" keeps its name.
|
||||
void divide_query_pair(const char *b, const char *e, std::string &key,
|
||||
std::string &val) {
|
||||
divide(b, static_cast<std::size_t>(e - b), '=',
|
||||
[&](const char *lhs_data, std::size_t lhs_size, const char *rhs_data,
|
||||
std::size_t rhs_size) {
|
||||
key.assign(lhs_data, lhs_size);
|
||||
val.assign(rhs_data, rhs_size);
|
||||
});
|
||||
}
|
||||
|
||||
void parse_query_text(const char *data, std::size_t size,
|
||||
Params ¶ms) {
|
||||
std::set<std::string> cache;
|
||||
@@ -3855,12 +3960,7 @@ void parse_query_text(const char *data, std::size_t size,
|
||||
|
||||
std::string key;
|
||||
std::string val;
|
||||
divide(b, static_cast<std::size_t>(e - b), '=',
|
||||
[&](const char *lhs_data, std::size_t lhs_size, const char *rhs_data,
|
||||
std::size_t rhs_size) {
|
||||
key.assign(lhs_data, lhs_size);
|
||||
val.assign(rhs_data, rhs_size);
|
||||
});
|
||||
divide_query_pair(b, e, key, val);
|
||||
|
||||
if (!key.empty()) {
|
||||
params.emplace(decode_query_component(key), decode_query_component(val));
|
||||
@@ -3874,20 +3974,18 @@ void parse_query_text(const std::string &s, Params ¶ms) {
|
||||
|
||||
// Normalize a query string by decoding and re-encoding each key/value pair
|
||||
// while preserving the original parameter order. This avoids double-encoding
|
||||
// and ensures consistent encoding without reordering (unlike Params which
|
||||
// uses std::multimap and sorts keys).
|
||||
// and ensures consistent encoding. It works on the raw string rather than
|
||||
// parsing into Params and re-serializing, because that round trip cannot
|
||||
// reproduce the input: params_to_query_str() always emits '=', so a bare
|
||||
// "flag" would come back as "flag=", and parse_query_text() drops exactly
|
||||
// duplicated pairs.
|
||||
std::string normalize_query_string(const std::string &query) {
|
||||
std::string result;
|
||||
split(query.data(), query.data() + query.size(), '&',
|
||||
[&](const char *b, const char *e) {
|
||||
std::string key;
|
||||
std::string val;
|
||||
divide(b, static_cast<std::size_t>(e - b), '=',
|
||||
[&](const char *lhs_data, std::size_t lhs_size,
|
||||
const char *rhs_data, std::size_t rhs_size) {
|
||||
key.assign(lhs_data, lhs_size);
|
||||
val.assign(rhs_data, rhs_size);
|
||||
});
|
||||
divide_query_pair(b, e, key, val);
|
||||
|
||||
if (!key.empty()) {
|
||||
auto dec_key = decode_query_component(key);
|
||||
@@ -3904,6 +4002,43 @@ std::string normalize_query_string(const std::string &query) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Build the request target that goes on the wire from a caller-supplied path.
|
||||
// Shared by the buffered send path and the streaming API so that both put the
|
||||
// same bytes in the request line for the same input.
|
||||
std::string encode_request_target(const std::string &target,
|
||||
bool path_encode) {
|
||||
// `substr(0, npos)` yields the whole string, which is what the no-query
|
||||
// case needs.
|
||||
auto query_pos = target.find('?');
|
||||
auto path_part = target.substr(0, query_pos);
|
||||
std::string query_part;
|
||||
if (query_pos != std::string::npos) {
|
||||
query_part = target.substr(query_pos + 1);
|
||||
}
|
||||
|
||||
auto result = path_encode ? encode_path(path_part) : std::move(path_part);
|
||||
|
||||
if (!query_part.empty()) {
|
||||
// When path encoding is disabled the caller has supplied an already-encoded
|
||||
// target and expects the exact bytes to be sent on the wire, so skip
|
||||
// normalization for the query too. Normalizing would decode-then-re-encode
|
||||
// it and corrupt pre-encoded binary payloads (e.g. turning `%20` into `+`,
|
||||
// which a strict RFC 3986 server decodes back as `+`, not a space).
|
||||
if (path_encode) {
|
||||
auto normalized = normalize_query_string(query_part);
|
||||
if (!normalized.empty()) {
|
||||
result += '?';
|
||||
result += normalized;
|
||||
}
|
||||
} else {
|
||||
result += '?';
|
||||
result += query_part;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool parse_multipart_boundary(const std::string &content_type,
|
||||
std::string &boundary) {
|
||||
std::map<std::string, std::string> params;
|
||||
@@ -4969,21 +5104,8 @@ bool is_field_valid(const std::string &name, const std::string &value) {
|
||||
|
||||
} // namespace fields
|
||||
|
||||
bool perform_websocket_handshake(Stream &strm, const std::string &host,
|
||||
int port, bool is_ssl,
|
||||
const std::string &path,
|
||||
const Headers &headers,
|
||||
bool perform_websocket_handshake(Stream &strm, Request &req,
|
||||
std::string &selected_subprotocol) {
|
||||
// Validate path and host
|
||||
if (!fields::is_field_value(path) || !fields::is_field_value(host)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate user-provided headers
|
||||
for (const auto &h : headers) {
|
||||
if (!fields::is_field_valid(h.first, h.second)) { return false; }
|
||||
}
|
||||
|
||||
// Generate random Sec-WebSocket-Key
|
||||
thread_local std::mt19937 rng(std::random_device{}());
|
||||
std::string key_bytes(16, '\0');
|
||||
@@ -4993,19 +5115,30 @@ bool perform_websocket_handshake(Stream &strm, const std::string &host,
|
||||
}
|
||||
auto client_key = base64_encode(key_bytes);
|
||||
|
||||
// Build upgrade request
|
||||
std::string req_str = "GET " + path + " HTTP/1.1\r\n";
|
||||
req_str += "Host: " + make_host_and_port_string(host, port, is_ssl) + "\r\n";
|
||||
req_str += "Upgrade: websocket\r\n";
|
||||
req_str += "Connection: Upgrade\r\n";
|
||||
req_str += "Sec-WebSocket-Key: " + client_key + "\r\n";
|
||||
req_str += "Sec-WebSocket-Version: 13\r\n";
|
||||
for (const auto &h : headers) {
|
||||
req_str += h.first + ": " + h.second + "\r\n";
|
||||
}
|
||||
req_str += "\r\n";
|
||||
req.headers.erase("Upgrade");
|
||||
req.headers.erase("Connection");
|
||||
req.headers.erase("Sec-WebSocket-Key");
|
||||
req.headers.erase("Sec-WebSocket-Version");
|
||||
req.headers.emplace("Upgrade", "websocket");
|
||||
req.headers.emplace("Connection", "Upgrade");
|
||||
req.headers.emplace("Sec-WebSocket-Key", client_key);
|
||||
req.headers.emplace("Sec-WebSocket-Version", "13");
|
||||
|
||||
if (strm.write(req_str.data(), req_str.size()) < 0) { return false; }
|
||||
// Build the request in memory first, like ClientImpl::write_request does.
|
||||
// Writing straight to the socket would leak a request line onto the wire
|
||||
// before check_and_write_headers gets a chance to reject an invalid header,
|
||||
// and would emit one small write per header.
|
||||
BufferStream bstrm;
|
||||
|
||||
if (write_request_line(bstrm, req.method, req.path) < 0) { return false; }
|
||||
|
||||
auto error = Error::Success;
|
||||
if (!check_and_write_headers(bstrm, req.headers, write_headers, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto &data = bstrm.get_buffer();
|
||||
if (!write_data(strm, data.data(), data.size())) { return false; }
|
||||
|
||||
// Verify 101 response and Sec-WebSocket-Accept header
|
||||
auto expected_accept = websocket_accept_key(client_key);
|
||||
@@ -5013,6 +5146,39 @@ bool perform_websocket_handshake(Stream &strm, const std::string &host,
|
||||
selected_subprotocol);
|
||||
}
|
||||
|
||||
bool is_ip_address(const std::string &host) {
|
||||
struct in_addr addr4;
|
||||
struct in6_addr addr6;
|
||||
return inet_pton(AF_INET, host.c_str(), &addr4) == 1 ||
|
||||
inet_pton(AF_INET6, host.c_str(), &addr6) == 1;
|
||||
}
|
||||
|
||||
// Resolve where a client should connect for `host`, honoring a user-supplied
|
||||
// hostname-to-address map. `host` itself is never rewritten, so it keeps
|
||||
// supplying the Host header and SNI; only the connection target changes.
|
||||
//
|
||||
// A mapped IP literal goes to `ip`, which keeps create_socket's AI_NUMERICHOST
|
||||
// path. Anything else goes to `connect_host`, which create_socket resolves as
|
||||
// a name, or uses as the socket path when the address family is AF_UNIX. An
|
||||
// absent or empty mapping leaves `host` as the connection target; without the
|
||||
// empty check the value would reach getaddrinfo as a null node and silently
|
||||
// resolve to loopback.
|
||||
void apply_addr_map(const std::map<std::string, std::string> &addr_map,
|
||||
const std::string &host, std::string &connect_host,
|
||||
std::string &ip) {
|
||||
connect_host = host;
|
||||
ip.clear();
|
||||
|
||||
auto it = addr_map.find(host);
|
||||
if (it == addr_map.end() || it->second.empty()) { return; }
|
||||
|
||||
if (is_ip_address(it->second)) {
|
||||
ip = it->second;
|
||||
} else {
|
||||
connect_host = it->second;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/*
|
||||
@@ -5044,7 +5210,12 @@ public:
|
||||
time_t duration() const override;
|
||||
void set_read_timeout(time_t sec, time_t usec = 0) override;
|
||||
|
||||
// See SocketStream::set_readable_hint().
|
||||
void set_readable_hint() { readable_hint_ = true; }
|
||||
|
||||
private:
|
||||
bool ensure_readable();
|
||||
|
||||
socket_t sock_;
|
||||
tls::session_t session_;
|
||||
time_t read_timeout_sec_;
|
||||
@@ -5053,6 +5224,7 @@ private:
|
||||
time_t write_timeout_usec_;
|
||||
time_t max_timeout_msec_;
|
||||
const std::chrono::time_point<std::chrono::steady_clock> start_time_;
|
||||
bool readable_hint_ = false;
|
||||
};
|
||||
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
@@ -5196,13 +5368,6 @@ std::string SHA_512(const std::string &s) {
|
||||
}
|
||||
#endif
|
||||
|
||||
bool is_ip_address(const std::string &host) {
|
||||
struct in_addr addr4;
|
||||
struct in6_addr addr6;
|
||||
return inet_pton(AF_INET, host.c_str(), &addr4) == 1 ||
|
||||
inet_pton(AF_INET6, host.c_str(), &addr6) == 1;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool process_server_socket_ssl(
|
||||
const std::atomic<socket_t> &svr_sock, tls::session_t session,
|
||||
@@ -5214,6 +5379,8 @@ bool process_server_socket_ssl(
|
||||
[&](bool close_connection, bool &connection_closed) {
|
||||
SSLSocketStream strm(sock, session, read_timeout_sec, read_timeout_usec,
|
||||
write_timeout_sec, write_timeout_usec);
|
||||
// See the non-TLS path in process_server_socket().
|
||||
strm.set_readable_hint();
|
||||
return callback(strm, close_connection, connection_closed);
|
||||
});
|
||||
}
|
||||
@@ -5665,6 +5832,7 @@ std::string to_string(const Error error) {
|
||||
case Error::UnsupportedAddressFamily: return "Unsupported address family";
|
||||
case Error::HTTPParsing: return "HTTP parsing failed";
|
||||
case Error::InvalidRangeHeader: return "Invalid Range header";
|
||||
case Error::UnsupportedContentEncoding: return "Unsupported Content-Encoding";
|
||||
default: break;
|
||||
}
|
||||
|
||||
@@ -6046,8 +6214,7 @@ std::string Request::get_trailer_value(const std::string &key,
|
||||
}
|
||||
|
||||
size_t Request::get_trailer_value_count(const std::string &key) const {
|
||||
auto r = trailers.equal_range(key);
|
||||
return static_cast<size_t>(std::distance(r.first, r.second));
|
||||
return trailers.count(key);
|
||||
}
|
||||
|
||||
bool Request::has_param(const std::string &key) const {
|
||||
@@ -6071,8 +6238,7 @@ Request::get_param_values(const std::string &key) const {
|
||||
}
|
||||
|
||||
size_t Request::get_param_value_count(const std::string &key) const {
|
||||
auto r = params.equal_range(key);
|
||||
return static_cast<size_t>(std::distance(r.first, r.second));
|
||||
return params.count(key);
|
||||
}
|
||||
|
||||
bool Request::is_multipart_form_data() const {
|
||||
@@ -6105,8 +6271,7 @@ bool MultipartFormData::has_field(const std::string &key) const {
|
||||
}
|
||||
|
||||
size_t MultipartFormData::get_field_count(const std::string &key) const {
|
||||
auto r = fields.equal_range(key);
|
||||
return static_cast<size_t>(std::distance(r.first, r.second));
|
||||
return fields.count(key);
|
||||
}
|
||||
|
||||
FormData MultipartFormData::get_file(const std::string &key,
|
||||
@@ -6129,8 +6294,7 @@ bool MultipartFormData::has_file(const std::string &key) const {
|
||||
}
|
||||
|
||||
size_t MultipartFormData::get_file_count(const std::string &key) const {
|
||||
auto r = files.equal_range(key);
|
||||
return static_cast<size_t>(std::distance(r.first, r.second));
|
||||
return files.count(key);
|
||||
}
|
||||
|
||||
// Multipart FormData writer implementation
|
||||
@@ -6209,8 +6373,7 @@ std::string Response::get_trailer_value(const std::string &key,
|
||||
}
|
||||
|
||||
size_t Response::get_trailer_value_count(const std::string &key) const {
|
||||
auto r = trailers.equal_range(key);
|
||||
return static_cast<size_t>(std::distance(r.first, r.second));
|
||||
return trailers.count(key);
|
||||
}
|
||||
|
||||
void Response::set_redirect(const std::string &url, int stat) {
|
||||
@@ -6306,8 +6469,7 @@ std::string Result::get_request_header_value(const std::string &key,
|
||||
|
||||
size_t
|
||||
Result::get_request_header_value_count(const std::string &key) const {
|
||||
auto r = request_headers_.equal_range(key);
|
||||
return static_cast<size_t>(std::distance(r.first, r.second));
|
||||
return request_headers_.count(key);
|
||||
}
|
||||
|
||||
// Stream implementation
|
||||
@@ -6595,6 +6757,24 @@ bool SocketStream::wait_writable() const {
|
||||
return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0;
|
||||
}
|
||||
|
||||
bool SocketStream::ensure_readable() {
|
||||
if (readable_hint_) {
|
||||
readable_hint_ = false;
|
||||
return true;
|
||||
}
|
||||
return wait_readable();
|
||||
}
|
||||
|
||||
const char *SocketStream::buffered_data(size_t &size) const {
|
||||
size = read_buff_content_size_ - read_buff_off_;
|
||||
return size ? read_buff_.data() + read_buff_off_ : nullptr;
|
||||
}
|
||||
|
||||
void SocketStream::consume_buffered(size_t size) {
|
||||
assert(size <= read_buff_content_size_ - read_buff_off_);
|
||||
read_buff_off_ += size;
|
||||
}
|
||||
|
||||
bool SocketStream::is_peer_alive() const {
|
||||
return detail::is_socket_alive(sock_);
|
||||
}
|
||||
@@ -6621,7 +6801,7 @@ ssize_t SocketStream::read(char *ptr, size_t size) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!wait_readable()) {
|
||||
if (!ensure_readable()) {
|
||||
error_ = Error::Timeout;
|
||||
return -1;
|
||||
}
|
||||
@@ -7099,6 +7279,14 @@ bool SSLSocketStream::wait_writable() const {
|
||||
!tls::is_peer_closed(session_, sock_);
|
||||
}
|
||||
|
||||
bool SSLSocketStream::ensure_readable() {
|
||||
if (readable_hint_) {
|
||||
readable_hint_ = false;
|
||||
return true;
|
||||
}
|
||||
return wait_readable();
|
||||
}
|
||||
|
||||
bool SSLSocketStream::is_peer_alive() const {
|
||||
return !tls::is_peer_closed(session_, sock_);
|
||||
}
|
||||
@@ -7111,7 +7299,7 @@ ssize_t SSLSocketStream::read(char *ptr, size_t size) {
|
||||
error_ = Error::ConnectionClosed;
|
||||
}
|
||||
return ret;
|
||||
} else if (wait_readable()) {
|
||||
} else if (ensure_readable()) {
|
||||
tls::TlsError err;
|
||||
auto ret = tls::read(session_, ptr, size, err);
|
||||
if (ret < 0) {
|
||||
@@ -7533,9 +7721,11 @@ void Server::wait_until_ready() const {
|
||||
}
|
||||
|
||||
void Server::stop() noexcept {
|
||||
if (is_running_) {
|
||||
assert(svr_sock_ != INVALID_SOCKET);
|
||||
std::atomic<socket_t> sock(svr_sock_.exchange(INVALID_SOCKET));
|
||||
// Release the listening socket whether or not the accept loop is running:
|
||||
// bind_to_port() without listen_after_bind() still owns the descriptor. The
|
||||
// exchange is what makes this safe to call concurrently with the accept loop.
|
||||
socket_t sock = svr_sock_.exchange(INVALID_SOCKET);
|
||||
if (sock != INVALID_SOCKET) {
|
||||
detail::shutdown_socket(sock);
|
||||
detail::close_socket(sock);
|
||||
}
|
||||
@@ -7697,7 +7887,15 @@ Server::write_content_with_provider(Stream &strm, const Request &req,
|
||||
};
|
||||
|
||||
if (res.content_length_ > 0) {
|
||||
if (req.ranges.empty()) {
|
||||
// Only a 206 response is served as a partial representation, matching the
|
||||
// condition `apply_ranges()` used to decide the Content-Length and the
|
||||
// multipart boundary. Since `detail::range_error()` validates `req.ranges`
|
||||
// only for a 2xx status, slicing under any other status would write a body
|
||||
// that disagrees with the header already sent, from an unchecked offset.
|
||||
auto is_partial =
|
||||
!req.ranges.empty() && res.status == StatusCode::PartialContent_206;
|
||||
|
||||
if (!is_partial) {
|
||||
return detail::write_content(strm, res.content_provider_, 0,
|
||||
res.content_length_, is_shutting_down);
|
||||
} else if (req.ranges.size() == 1) {
|
||||
@@ -8096,7 +8294,14 @@ int Server::bind_internal(const std::string &host, int port,
|
||||
}
|
||||
|
||||
bool Server::listen_internal() {
|
||||
if (is_decommissioned) { return false; }
|
||||
// A stop() between bind and listen leaves nothing to accept on. Report
|
||||
// failure instead of returning success without ever serving, and mark the
|
||||
// server decommissioned the way any failed listen does so that a concurrent
|
||||
// wait_until_ready() wakes up instead of spinning forever.
|
||||
if (is_decommissioned || svr_sock_ == INVALID_SOCKET) {
|
||||
is_decommissioned = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto ret = true;
|
||||
is_running_ = true;
|
||||
@@ -8492,11 +8697,17 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
return write_response(strm, close_connection, req, res);
|
||||
}
|
||||
|
||||
// RFC 9112 §6.3: Reject requests with both a non-zero Content-Length and
|
||||
// any Transfer-Encoding to prevent request smuggling. Content-Length: 0 is
|
||||
// tolerated for compatibility with existing clients.
|
||||
if (req.get_header_value_u64("Content-Length") > 0 &&
|
||||
req.has_header("Transfer-Encoding")) {
|
||||
// RFC 9112 §6.3: Reject requests whose framing is ambiguous, which would
|
||||
// otherwise let an intermediary and this parser disagree on where the body
|
||||
// ends and enable request smuggling. Two cases: a non-zero Content-Length
|
||||
// alongside any Transfer-Encoding (Content-Length: 0 is tolerated for
|
||||
// compatibility with existing clients), and a Transfer-Encoding whose final
|
||||
// coding is not chunked, which leaves the body length undeterminable. The
|
||||
// latter must not fall through to the "no body" path, or the body bytes are
|
||||
// parsed as the next request on a persistent connection.
|
||||
if (req.has_header("Transfer-Encoding") &&
|
||||
(req.get_header_value_u64("Content-Length") > 0 ||
|
||||
!detail::is_chunked_transfer_encoding(req.headers))) {
|
||||
connection_closed = true;
|
||||
res.status = StatusCode::BadRequest_400;
|
||||
return write_response(strm, close_connection, req, res);
|
||||
@@ -8908,13 +9119,13 @@ socket_t ClientImpl::create_client_socket(Error &error) const {
|
||||
write_timeout_sec_, write_timeout_usec_, interface_, error);
|
||||
}
|
||||
|
||||
// Check is custom IP specified for host_
|
||||
// Check is custom IP or hostname specified for host_
|
||||
std::string connect_host;
|
||||
std::string ip;
|
||||
auto it = addr_map_.find(host_);
|
||||
if (it != addr_map_.end()) { ip = it->second; }
|
||||
detail::apply_addr_map(addr_map_, host_, connect_host, ip);
|
||||
|
||||
return detail::create_client_socket(
|
||||
host_, ip, port_, address_family_, tcp_nodelay_, ipv6_v6only_,
|
||||
connect_host, ip, port_, address_family_, tcp_nodelay_, ipv6_v6only_,
|
||||
socket_options_, connection_timeout_sec_, connection_timeout_usec_,
|
||||
read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
|
||||
write_timeout_usec_, interface_, error);
|
||||
@@ -9142,11 +9353,13 @@ void ClientImpl::prepare_default_headers(Request &r, bool for_stream,
|
||||
if (!r.has_header(header.first)) { r.headers.insert(header); }
|
||||
}
|
||||
|
||||
// RFC 9110 5.3 recommends sending control data such as Host first, so
|
||||
// prepend it rather than appending it after the caller's own fields.
|
||||
if (!r.has_header("Host")) {
|
||||
if (address_family_ == AF_UNIX) {
|
||||
r.headers.emplace("Host", "localhost");
|
||||
r.headers.emplace_front("Host", "localhost");
|
||||
} else {
|
||||
r.headers.emplace(
|
||||
r.headers.emplace_front(
|
||||
"Host", detail::make_host_and_port_string(host_, port_, is_ssl()));
|
||||
}
|
||||
}
|
||||
@@ -9197,7 +9410,12 @@ ClientImpl::open_stream(const std::string &method, const std::string &path,
|
||||
handle.response = detail::make_unique<Response>();
|
||||
handle.error = Error::Success;
|
||||
|
||||
auto query_path = params.empty() ? path : append_query_params(path, params);
|
||||
// Encode the target exactly like the buffered send path does, so that the
|
||||
// same `path` produces the same request line through either API.
|
||||
auto raw_query_path =
|
||||
params.empty() ? path : append_query_params(path, params);
|
||||
auto query_path = detail::encode_request_target(raw_query_path, path_encode_);
|
||||
|
||||
handle.connection_ = detail::make_unique<ClientConnection>();
|
||||
|
||||
{
|
||||
@@ -9311,7 +9529,20 @@ ClientImpl::open_stream(const std::string &method, const std::string &path,
|
||||
|
||||
auto content_encoding = handle.response->get_header_value("Content-Encoding");
|
||||
if (!content_encoding.empty()) {
|
||||
// Same policy as prepare_content_receiver(): reject a coding we know about
|
||||
// but were not built with, pass an unrecognized one through as-is.
|
||||
handle.decompressor_ = detail::create_decompressor(content_encoding);
|
||||
if (!handle.decompressor_) {
|
||||
if (detail::is_known_content_encoding(content_encoding)) {
|
||||
handle.error = Error::UnsupportedContentEncoding;
|
||||
handle.response.reset();
|
||||
return handle;
|
||||
}
|
||||
} else if (!handle.decompressor_->is_valid()) {
|
||||
handle.error = Error::Compression;
|
||||
handle.response.reset();
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
|
||||
return handle;
|
||||
@@ -9842,52 +10073,26 @@ bool ClientImpl::write_request(Stream &strm, Request &req,
|
||||
{
|
||||
detail::BufferStream bstrm;
|
||||
|
||||
// Extract path and query from req.path
|
||||
std::string path_part, query_part;
|
||||
// Extract the query from req.path. The encoding itself is delegated to
|
||||
// `encode_request_target`; the raw query is still needed here to decide
|
||||
// between populating `req.params` from it and falling back to building a
|
||||
// query out of caller-supplied `req.params`.
|
||||
auto query_pos = req.path.find('?');
|
||||
if (query_pos != std::string::npos) {
|
||||
path_part = req.path.substr(0, query_pos);
|
||||
query_part = req.path.substr(query_pos + 1);
|
||||
} else {
|
||||
path_part = req.path;
|
||||
query_part = "";
|
||||
}
|
||||
auto query_part = query_pos == std::string::npos
|
||||
? std::string()
|
||||
: req.path.substr(query_pos + 1);
|
||||
|
||||
// Encode path part. If the original `req.path` already contained a
|
||||
// query component, preserve its raw query string (including parameter
|
||||
// order) instead of reparsing and reassembling it which may reorder
|
||||
// parameters due to container ordering (e.g. `Params` uses
|
||||
// `std::multimap`). When there is no query in `req.path`, fall back to
|
||||
// building a query from `req.params` so existing callers that pass
|
||||
// `Params` continue to work.
|
||||
auto path_with_query =
|
||||
path_encode_ ? detail::encode_path(path_part) : path_part;
|
||||
detail::encode_request_target(req.path, path_encode_);
|
||||
|
||||
if (!query_part.empty()) {
|
||||
// Normalize the query string (decode then re-encode) while preserving
|
||||
// the original parameter order. When path encoding is disabled the
|
||||
// caller has supplied an already-encoded target and expects the exact
|
||||
// bytes to be sent on the wire, so skip normalization for the query
|
||||
// too. Normalizing here would decode-then-re-encode the query and
|
||||
// corrupt pre-encoded binary payloads (e.g. turning `%20` into `+`,
|
||||
// which a strict RFC 3986 server decodes back as `+`, not a space).
|
||||
if (path_encode_) {
|
||||
auto normalized = detail::normalize_query_string(query_part);
|
||||
if (!normalized.empty()) { path_with_query += '?' + normalized; }
|
||||
} else {
|
||||
path_with_query += '?' + query_part;
|
||||
}
|
||||
|
||||
// Still populate req.params for handlers/users who read them.
|
||||
// The query already came in through `req.path`; still populate
|
||||
// `req.params` for handlers/users who read them.
|
||||
detail::parse_query_text(query_part, req.params);
|
||||
} else {
|
||||
// No query in path; parse any query_part (empty) and append params
|
||||
// from `req.params` when present (preserves prior behavior for
|
||||
// callers who provide Params separately).
|
||||
detail::parse_query_text(query_part, req.params);
|
||||
if (!req.params.empty()) {
|
||||
path_with_query = append_query_params(path_with_query, req.params);
|
||||
}
|
||||
} else if (!req.params.empty()) {
|
||||
// No query in `req.path`; build one from `req.params` so existing
|
||||
// callers that pass `Params` separately continue to work.
|
||||
path_with_query = append_query_params(path_with_query, req.params);
|
||||
}
|
||||
|
||||
// Write request line and headers
|
||||
@@ -10298,14 +10503,26 @@ bool ClientImpl::process_request(Stream &strm, Request &req,
|
||||
}
|
||||
|
||||
if (res.status != StatusCode::NotModified_304) {
|
||||
int dummy_status;
|
||||
auto content_status = 0;
|
||||
auto max_length = (!has_payload_max_length_ && req.content_receiver)
|
||||
? (std::numeric_limits<size_t>::max)()
|
||||
: payload_max_length_;
|
||||
if (!detail::read_content(strm, res, max_length, dummy_status,
|
||||
if (!detail::read_content(strm, res, max_length, content_status,
|
||||
std::move(progress), std::move(out),
|
||||
decompress_)) {
|
||||
if (error != Error::Canceled) { error = Error::Read; }
|
||||
if (error != Error::Canceled) {
|
||||
// Tell the caller apart from a plain read failure when the body could
|
||||
// not be decoded because of its Content-Encoding.
|
||||
switch (content_status) {
|
||||
case StatusCode::UnsupportedMediaType_415:
|
||||
error = Error::UnsupportedContentEncoding;
|
||||
break;
|
||||
case StatusCode::InternalServerError_500:
|
||||
error = Error::Compression;
|
||||
break;
|
||||
default: error = Error::Read; break;
|
||||
}
|
||||
}
|
||||
output_error_log(error, &req);
|
||||
return false;
|
||||
}
|
||||
@@ -16769,18 +16986,42 @@ bool WebSocketClient::create_stream(std::unique_ptr<Stream> &strm) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void WebSocketClient::prepare_default_headers(Request &req) {
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
auto is_ssl = is_ssl_;
|
||||
#else
|
||||
auto is_ssl = false;
|
||||
#endif
|
||||
|
||||
if (!req.has_header("Host")) {
|
||||
if (address_family_ == AF_UNIX) {
|
||||
req.headers.emplace("Host", "localhost");
|
||||
} else {
|
||||
req.headers.emplace(
|
||||
"Host", detail::make_host_and_port_string(host_, port_, is_ssl));
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef CPPHTTPLIB_NO_DEFAULT_USER_AGENT
|
||||
if (!req.has_header("User-Agent")) {
|
||||
auto agent = std::string("cpp-httplib/") + CPPHTTPLIB_VERSION;
|
||||
req.set_header("User-Agent", agent);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool WebSocketClient::connect() {
|
||||
if (!is_valid_) { return false; }
|
||||
shutdown_and_close();
|
||||
|
||||
// Check is custom IP specified for host_
|
||||
// Check is custom IP or hostname specified for host_
|
||||
std::string connect_host;
|
||||
std::string ip;
|
||||
auto it = addr_map_.find(host_);
|
||||
if (it != addr_map_.end()) { ip = it->second; }
|
||||
detail::apply_addr_map(addr_map_, host_, connect_host, ip);
|
||||
|
||||
Error error;
|
||||
sock_ = detail::create_client_socket(
|
||||
host_, ip, port_, address_family_, tcp_nodelay_, ipv6_v6only_,
|
||||
connect_host, ip, port_, address_family_, tcp_nodelay_, ipv6_v6only_,
|
||||
socket_options_, connection_timeout_sec_, connection_timeout_usec_,
|
||||
read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
|
||||
write_timeout_usec_, interface_, error);
|
||||
@@ -16793,23 +17034,19 @@ bool WebSocketClient::connect() {
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
auto is_ssl = is_ssl_;
|
||||
#else
|
||||
auto is_ssl = false;
|
||||
#endif
|
||||
Request req;
|
||||
req.method = "GET";
|
||||
req.path = path_;
|
||||
req.headers = headers_;
|
||||
prepare_default_headers(req);
|
||||
|
||||
std::string selected_subprotocol;
|
||||
if (!detail::perform_websocket_handshake(*strm, host_, port_, is_ssl, path_,
|
||||
headers_, selected_subprotocol)) {
|
||||
if (!detail::perform_websocket_handshake(*strm, req, selected_subprotocol)) {
|
||||
shutdown_and_close();
|
||||
return false;
|
||||
}
|
||||
subprotocol_ = std::move(selected_subprotocol);
|
||||
|
||||
Request req;
|
||||
req.method = "GET";
|
||||
req.path = path_;
|
||||
ws_ = std::unique_ptr<WebSocket>(new WebSocket(std::move(strm), req, false,
|
||||
websocket_ping_interval_sec_,
|
||||
websocket_max_missed_pongs_));
|
||||
|
||||
Vendored
+322
-11
@@ -8,8 +8,8 @@
|
||||
#ifndef CPPHTTPLIB_HTTPLIB_H
|
||||
#define CPPHTTPLIB_HTTPLIB_H
|
||||
|
||||
#define CPPHTTPLIB_VERSION "0.51.0"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x003300"
|
||||
#define CPPHTTPLIB_VERSION "0.52.0"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x003400"
|
||||
|
||||
#ifdef _WIN32
|
||||
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
||||
@@ -182,7 +182,7 @@
|
||||
#endif
|
||||
|
||||
#ifndef CPPHTTPLIB_LISTEN_BACKLOG
|
||||
#define CPPHTTPLIB_LISTEN_BACKLOG 5
|
||||
#define CPPHTTPLIB_LISTEN_BACKLOG 128
|
||||
#endif
|
||||
|
||||
#ifndef CPPHTTPLIB_MAX_LINE_LENGTH
|
||||
@@ -321,6 +321,7 @@ using socket_t = int;
|
||||
#include <functional>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
@@ -333,9 +334,11 @@ using socket_t = int;
|
||||
#include <sys/stat.h>
|
||||
#include <system_error>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
// On macOS with a TLS backend, enable Keychain root certificates by default
|
||||
// unless the user explicitly opts out. Not enabled on iOS/tvOS/watchOS since
|
||||
@@ -968,11 +971,291 @@ enum StatusCode {
|
||||
NetworkAuthenticationRequired_511 = 511,
|
||||
};
|
||||
|
||||
using Headers =
|
||||
std::unordered_multimap<std::string, std::string, detail::case_ignore::hash,
|
||||
detail::case_ignore::equal_to>;
|
||||
namespace detail {
|
||||
|
||||
using Params = std::multimap<std::string, std::string>;
|
||||
// A multimap that keeps its entries in the order they were inserted.
|
||||
//
|
||||
// HTTP needs that order in two places. RFC 9110 5.3 makes the order of header
|
||||
// fields sharing a field name significant and forbids a proxy from reordering
|
||||
// them, and a query string's parameters are meaningful in the order the caller
|
||||
// wrote them. Neither standard container expresses it: std::unordered_multimap
|
||||
// gives no ordering guarantee at all for equivalent keys (libstdc++ yields
|
||||
// reverse insertion order, libc++ insertion order), and std::multimap sorts by
|
||||
// key, which would drop control data such as Host behind whatever else the
|
||||
// message carries and alphabetise a query string.
|
||||
//
|
||||
// Entries are therefore kept in a flat vector, in order. Lookup is a linear
|
||||
// scan, which beats hashing for the handful of entries a message carries
|
||||
// (headers are capped at CPPHTTPLIB_HEADER_MAX_COUNT).
|
||||
//
|
||||
// KeyEqual compares keys; it is what makes Headers case-insensitive and
|
||||
// Params, whose parameter names are case-sensitive, not.
|
||||
template <typename Mapped, typename KeyEqual> class insertion_ordered_multimap {
|
||||
public:
|
||||
using key_type = std::string;
|
||||
using mapped_type = Mapped;
|
||||
using value_type = std::pair<std::string, Mapped>;
|
||||
using size_type = std::size_t;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
using reference = value_type &;
|
||||
using const_reference = const value_type &;
|
||||
|
||||
private:
|
||||
static size_type npos() { return static_cast<size_type>(-1); }
|
||||
|
||||
static bool keys_equal(const std::string &a, const std::string &b) {
|
||||
return KeyEqual()(a, b);
|
||||
}
|
||||
|
||||
// Iterating yields every entry in insertion order, but equal_range() and
|
||||
// find() have to walk only the entries sharing one key, which are not
|
||||
// adjacent. Both are the same iterator type: key_idx_ selects between the
|
||||
// two traversals, and since equality compares only the position, an iterator
|
||||
// restricted to one key still compares equal to end().
|
||||
template <typename V> class iterator_t {
|
||||
public:
|
||||
using iterator_category = std::bidirectional_iterator_tag;
|
||||
using value_type = insertion_ordered_multimap::value_type;
|
||||
using difference_type = insertion_ordered_multimap::difference_type;
|
||||
using pointer = V *;
|
||||
using reference = V &;
|
||||
|
||||
iterator_t() : data_(nullptr), idx_(0), size_(0), key_idx_(npos()) {}
|
||||
|
||||
template <typename U,
|
||||
typename std::enable_if<std::is_convertible<U *, V *>::value,
|
||||
int>::type = 0>
|
||||
iterator_t(const iterator_t<U> &rhs)
|
||||
: data_(rhs.data_), idx_(rhs.idx_), size_(rhs.size_),
|
||||
key_idx_(rhs.key_idx_) {}
|
||||
|
||||
reference operator*() const { return data_[idx_]; }
|
||||
pointer operator->() const { return data_ + idx_; }
|
||||
|
||||
iterator_t &operator++() {
|
||||
// Saturating, so that advancing past the last entry of a key (which
|
||||
// get_multimap_value() does when asked for an out-of-range id) stays at
|
||||
// end() instead of running off the container.
|
||||
if (idx_ >= size_) { return *this; }
|
||||
++idx_;
|
||||
if (key_idx_ != npos()) {
|
||||
while (idx_ < size_ && !matches(idx_)) {
|
||||
++idx_;
|
||||
}
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
iterator_t operator++(int) {
|
||||
auto tmp = *this;
|
||||
++*this;
|
||||
return tmp;
|
||||
}
|
||||
|
||||
iterator_t &operator--() {
|
||||
if (idx_ == 0) { return *this; }
|
||||
--idx_;
|
||||
if (key_idx_ != npos()) {
|
||||
while (idx_ > 0 && !matches(idx_)) {
|
||||
--idx_;
|
||||
}
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
iterator_t operator--(int) {
|
||||
auto tmp = *this;
|
||||
--*this;
|
||||
return tmp;
|
||||
}
|
||||
|
||||
template <typename U> bool operator==(const iterator_t<U> &rhs) const {
|
||||
return idx_ == rhs.idx_;
|
||||
}
|
||||
|
||||
template <typename U> bool operator!=(const iterator_t<U> &rhs) const {
|
||||
return idx_ != rhs.idx_;
|
||||
}
|
||||
|
||||
private:
|
||||
friend class insertion_ordered_multimap;
|
||||
template <typename> friend class iterator_t;
|
||||
|
||||
iterator_t(V *data, size_type idx, size_type size, size_type key_idx)
|
||||
: data_(data), idx_(idx), size_(size), key_idx_(key_idx) {}
|
||||
|
||||
bool matches(size_type i) const {
|
||||
return keys_equal(data_[i].first, data_[key_idx_].first);
|
||||
}
|
||||
|
||||
V *data_;
|
||||
size_type idx_;
|
||||
size_type size_;
|
||||
size_type key_idx_;
|
||||
};
|
||||
|
||||
public:
|
||||
using iterator = iterator_t<value_type>;
|
||||
using const_iterator = iterator_t<const value_type>;
|
||||
|
||||
insertion_ordered_multimap() = default;
|
||||
insertion_ordered_multimap(std::initializer_list<value_type> il)
|
||||
: entries_(il) {}
|
||||
template <typename InputIt>
|
||||
insertion_ordered_multimap(InputIt first, InputIt last)
|
||||
: entries_(first, last) {}
|
||||
|
||||
iterator begin() { return make_iter(0, npos()); }
|
||||
iterator end() { return make_iter(entries_.size(), npos()); }
|
||||
const_iterator begin() const { return make_citer(0, npos()); }
|
||||
const_iterator end() const { return make_citer(entries_.size(), npos()); }
|
||||
const_iterator cbegin() const { return begin(); }
|
||||
const_iterator cend() const { return end(); }
|
||||
|
||||
bool empty() const { return entries_.empty(); }
|
||||
size_type size() const { return entries_.size(); }
|
||||
void clear() { entries_.clear(); }
|
||||
void swap(insertion_ordered_multimap &rhs) { entries_.swap(rhs.entries_); }
|
||||
|
||||
iterator insert(const value_type &val) {
|
||||
entries_.push_back(val);
|
||||
return make_iter(entries_.size() - 1, npos());
|
||||
}
|
||||
|
||||
iterator insert(value_type &&val) {
|
||||
entries_.push_back(std::move(val));
|
||||
return make_iter(entries_.size() - 1, npos());
|
||||
}
|
||||
|
||||
template <typename... Args> iterator emplace(Args &&...args) {
|
||||
entries_.emplace_back(std::forward<Args>(args)...);
|
||||
return make_iter(entries_.size() - 1, npos());
|
||||
}
|
||||
|
||||
// For entries that have to lead the message, such as the Host header field
|
||||
// (RFC 9110 5.3 recommends sending control data first).
|
||||
template <typename... Args> iterator emplace_front(Args &&...args) {
|
||||
entries_.emplace(entries_.begin(), std::forward<Args>(args)...);
|
||||
return make_iter(0, npos());
|
||||
}
|
||||
|
||||
iterator find(const std::string &key) {
|
||||
auto i = index_of(key);
|
||||
return i == npos() ? end() : make_iter(i, i);
|
||||
}
|
||||
|
||||
const_iterator find(const std::string &key) const {
|
||||
auto i = index_of(key);
|
||||
return i == npos() ? end() : make_citer(i, i);
|
||||
}
|
||||
|
||||
size_type count(const std::string &key) const {
|
||||
size_type n = 0;
|
||||
for (const auto &entry : entries_) {
|
||||
if (keys_equal(entry.first, key)) { n++; }
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
std::pair<iterator, iterator> equal_range(const std::string &key) {
|
||||
auto i = index_of(key);
|
||||
return i == npos() ? std::make_pair(end(), end())
|
||||
: std::make_pair(make_iter(i, i), end());
|
||||
}
|
||||
|
||||
std::pair<const_iterator, const_iterator>
|
||||
equal_range(const std::string &key) const {
|
||||
auto i = index_of(key);
|
||||
return i == npos() ? std::make_pair(end(), end())
|
||||
: std::make_pair(make_citer(i, i), end());
|
||||
}
|
||||
|
||||
size_type erase(const std::string &key) {
|
||||
auto before = entries_.size();
|
||||
entries_.erase(std::remove_if(entries_.begin(), entries_.end(),
|
||||
[&](const value_type &entry) {
|
||||
return keys_equal(entry.first, key);
|
||||
}),
|
||||
entries_.end());
|
||||
return before - entries_.size();
|
||||
}
|
||||
|
||||
iterator erase(const_iterator pos) {
|
||||
entries_.erase(entries_.begin() + static_cast<difference_type>(pos.idx_));
|
||||
return make_iter(pos.idx_, npos());
|
||||
}
|
||||
|
||||
// Erases what iterating [first, last) would actually visit, so erasing an
|
||||
// equal_range() removes only the entries with that key, not everything
|
||||
// positioned between them.
|
||||
iterator erase(const_iterator first, const_iterator last) {
|
||||
auto from = first.idx_;
|
||||
auto to = last.idx_;
|
||||
if (from >= to) { return make_iter(from, npos()); }
|
||||
|
||||
auto begin_it = entries_.begin();
|
||||
auto from_it = begin_it + static_cast<difference_type>(from);
|
||||
auto to_it = begin_it + static_cast<difference_type>(to);
|
||||
|
||||
if (first.key_idx_ == npos()) {
|
||||
entries_.erase(from_it, to_it);
|
||||
} else {
|
||||
auto key = entries_[first.key_idx_].first;
|
||||
auto keep = from_it;
|
||||
for (auto it = from_it; it != to_it; ++it) {
|
||||
if (!keys_equal(it->first, key)) {
|
||||
if (keep != it) { *keep = std::move(*it); }
|
||||
++keep;
|
||||
}
|
||||
}
|
||||
if (keep != to_it) {
|
||||
keep = std::move(to_it, entries_.end(), keep);
|
||||
} else {
|
||||
keep = entries_.end();
|
||||
}
|
||||
entries_.erase(keep, entries_.end());
|
||||
}
|
||||
return make_iter(from, npos());
|
||||
}
|
||||
|
||||
friend bool operator==(const insertion_ordered_multimap &lhs,
|
||||
const insertion_ordered_multimap &rhs) {
|
||||
return lhs.entries_ == rhs.entries_;
|
||||
}
|
||||
|
||||
friend bool operator!=(const insertion_ordered_multimap &lhs,
|
||||
const insertion_ordered_multimap &rhs) {
|
||||
return !(lhs == rhs);
|
||||
}
|
||||
|
||||
private:
|
||||
size_type index_of(const std::string &key) const {
|
||||
for (size_type i = 0; i < entries_.size(); i++) {
|
||||
if (keys_equal(entries_[i].first, key)) { return i; }
|
||||
}
|
||||
return npos();
|
||||
}
|
||||
|
||||
iterator make_iter(size_type idx, size_type key_idx) {
|
||||
return iterator(entries_.data(), idx, entries_.size(), key_idx);
|
||||
}
|
||||
|
||||
const_iterator make_citer(size_type idx, size_type key_idx) const {
|
||||
return const_iterator(entries_.data(), idx, entries_.size(), key_idx);
|
||||
}
|
||||
|
||||
std::vector<value_type> entries_;
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
using Headers =
|
||||
detail::insertion_ordered_multimap<std::string,
|
||||
detail::case_ignore::equal_to>;
|
||||
|
||||
// Query parameter names are case-sensitive, unlike header field names.
|
||||
using Params =
|
||||
detail::insertion_ordered_multimap<std::string, std::equal_to<std::string>>;
|
||||
using Match = std::smatch;
|
||||
|
||||
using DownloadProgress = std::function<bool(size_t current, size_t total)>;
|
||||
@@ -1079,9 +1362,16 @@ struct FormField {
|
||||
std::string content;
|
||||
Headers headers;
|
||||
};
|
||||
using FormFields = std::multimap<std::string, FormField>;
|
||||
// RFC 7578 5.2: a form processor "SHOULD send back results in order" and
|
||||
// "Intermediaries MUST NOT reorder the results", so a handler walking these
|
||||
// should see the parts as they were sent. A std::multimap sorts by field name
|
||||
// and loses that. Field names are case-sensitive, hence std::equal_to rather
|
||||
// than the case-insensitive predicate Headers uses.
|
||||
using FormFields =
|
||||
detail::insertion_ordered_multimap<FormField, std::equal_to<std::string>>;
|
||||
|
||||
using FormFiles = std::multimap<std::string, FormData>;
|
||||
using FormFiles =
|
||||
detail::insertion_ordered_multimap<FormData, std::equal_to<std::string>>;
|
||||
|
||||
struct MultipartFormData {
|
||||
FormFields fields; // Text fields from multipart
|
||||
@@ -1514,6 +1804,7 @@ enum class Error {
|
||||
UnsupportedAddressFamily,
|
||||
HTTPParsing,
|
||||
InvalidRangeHeader,
|
||||
UnsupportedContentEncoding,
|
||||
|
||||
// For internal use only
|
||||
SSLPeerCouldBeClosed_,
|
||||
@@ -1545,6 +1836,18 @@ public:
|
||||
(void)usec;
|
||||
}
|
||||
|
||||
// Bytes already pulled off the socket and sitting in this stream's own
|
||||
// buffer. Exposing them lets a line reader scan for a terminator in one
|
||||
// pass instead of asking for a byte at a time. A stream that does no
|
||||
// buffering of its own reports none, and readers fall back to read().
|
||||
virtual const char *buffered_data(size_t &size) const {
|
||||
size = 0;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Discards `size` bytes previously returned by buffered_data().
|
||||
virtual void consume_buffered(size_t size) { (void)size; }
|
||||
|
||||
ssize_t write(const char *ptr);
|
||||
ssize_t write(const std::string &s);
|
||||
|
||||
@@ -2452,7 +2755,8 @@ protected:
|
||||
std::thread::id socket_requests_are_from_thread_ = std::thread::id();
|
||||
bool socket_should_be_closed_when_request_is_done_ = false;
|
||||
|
||||
// Hostname-IP map
|
||||
// Hostname to connection target map. The value is an IP literal or another
|
||||
// hostname; only the connection target changes, never the identity.
|
||||
std::map<std::string, std::string> addr_map_;
|
||||
|
||||
// Default headers
|
||||
@@ -3154,6 +3458,10 @@ private:
|
||||
std::string make_host_and_port_string(const std::string &host, int port,
|
||||
bool is_ssl);
|
||||
|
||||
template <typename T>
|
||||
bool check_and_write_headers(Stream &strm, Headers &headers, T header_writer,
|
||||
Error &error);
|
||||
|
||||
std::string trim_copy(const std::string &s);
|
||||
|
||||
void divide(
|
||||
@@ -3364,6 +3672,7 @@ public:
|
||||
|
||||
private:
|
||||
void append(char c);
|
||||
void append(const char *data, size_t size);
|
||||
|
||||
Stream &strm_;
|
||||
char *fixed_buffer_;
|
||||
@@ -3992,6 +4301,7 @@ public:
|
||||
private:
|
||||
void shutdown_and_close();
|
||||
bool create_stream(std::unique_ptr<Stream> &strm);
|
||||
void prepare_default_headers(Request &req);
|
||||
|
||||
std::string host_;
|
||||
int port_;
|
||||
@@ -4016,7 +4326,8 @@ private:
|
||||
time_t connection_timeout_usec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND;
|
||||
std::string interface_;
|
||||
|
||||
// Hostname-IP map
|
||||
// Hostname to connection target map. The value is an IP literal or another
|
||||
// hostname; only the connection target changes, never the identity.
|
||||
std::map<std::string, std::string> addr_map_;
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
|
||||
Reference in New Issue
Block a user