mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-08-23 22:17:34 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c060ca974c | ||
|
|
ccc8fd2baa | ||
|
|
d05f89562d | ||
|
|
8d9af25633 | ||
|
|
4a08fa2970 | ||
|
|
56db501e73 | ||
|
|
95b8e33e16 | ||
|
|
a278dcef04 | ||
|
|
e8eed4525a | ||
|
|
ba8e0eddfb |
@@ -74,6 +74,7 @@ For more info, please refer to the [AGENTS.md](AGENTS.md) file.
|
||||
- If a PR does not warrant a new release, add `[no release]` in the squashed commit to spare CI resources
|
||||
- Be mindful of maintenance: most of the work going into a feature happens after the PR is merged. If the PR author is not committed to contribute long-term, someone else needs to take responsibility (you)
|
||||
- Add the ["merge ready"](https://github.com/ggml-org/llama.cpp/pulls?q=is%3Apr+is%3Aopen+draft%3Ano+sort%3Aupdated-desc+label%3A%22merge+ready%22+) label to a PR to indicate when a PR can be fast-merged without waiting for 2 independent reviews. [(more info)](https://github.com/ggml-org/llama.cpp/pull/26178)
|
||||
- Wait for CI results before merging
|
||||
|
||||
Maintainers reserve the right to decline review or close pull requests for any reason, without any questions, particularly under any of the following conditions:
|
||||
- The proposed change is already mentioned in the roadmap or an existing issue, and it has been assigned to someone.
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
[](https://github.com/ggml-org/llama.cpp/actions/workflows/docker.yml)
|
||||
[](https://github.com/ggml-org/llama.cpp/actions/workflows/winget.yml)
|
||||
|
||||
[manifesto](https://github.com/ggml-org/llama.cpp/discussions/205) / [ggml](https://github.com/ggml-org/ggml) / [ops](https://github.com/ggml-org/llama.cpp/blob/master/docs/ops.md) / [maintainer PRs](https://github.com/ggml-org/llama.cpp/issues?q=is%3Apr%20is%3Aopen%20draft%3AFalse%20(author%3Argerganov%20OR%20author%3AKitaitiMakoto%20OR%20author%3Adanbev%20OR%20author%3Aaldehir%20OR%20author%3Amax-krasnyansky%20OR%20author%3ACISC%20OR%20author%3Aggerganov%20OR%20author%3Aam17an%20OR%20author%3Abartowski1182%20OR%20author%3Ahipudding%20OR%20author%3AServeurpersoCom%20OR%20author%3Apwilkin%20OR%20author%3Areeselevine%20OR%20author%3Angxson%20OR%20author%3Ajeffbolznv%20OR%20author%3A0cc4m%20OR%20author%3Aangt%20OR%20author%3AIMbackK%20OR%20author%3Aarthw%20OR%20author%3AJohannesGaessler%20OR%20author%3AORippler%20OR%20author%3Aruixiang63%20OR%20author%3Axctan%20OR%20author%3Aallozaur%20OR%20author%3Ayomaytk%20OR%20author%3Aaendk%20OR%20author%3Agaugarg-nv%20OR%20author%3Ataronaeo%20OR%20author%3Aforforever73%20OR%20author%3Alhez%20OR%20author%3Anetrunnereve%20OR%20author%3Afairydreaming)%20sort%3Aupdated-desc) / [compile times](https://github.com/ggml-org/llama.cpp-dev/blob/master/README-compile-times.md) / [lib llama API](https://github.com/ggml-org/llama.cpp/issues/9289) / [llama-server REST API](https://github.com/ggml-org/llama.cpp/issues/9291)
|
||||
[ggml](https://github.com/ggml-org/ggml) / [ops](https://github.com/ggml-org/llama.cpp/blob/master/docs/ops.md) / [maintainer PRs](https://github.com/ggml-org/llama.cpp/issues?q=is%3Apr%20is%3Aopen%20draft%3AFalse%20(author%3Argerganov%20OR%20author%3AKitaitiMakoto%20OR%20author%3Adanbev%20OR%20author%3Aaldehir%20OR%20author%3Amax-krasnyansky%20OR%20author%3ACISC%20OR%20author%3Aggerganov%20OR%20author%3Aam17an%20OR%20author%3Abartowski1182%20OR%20author%3Anikwen%20OR%20author%3Ahipudding%20OR%20author%3AServeurpersoCom%20OR%20author%3Apwilkin%20OR%20author%3Areeselevine%20OR%20author%3Angxson%20OR%20author%3Ajeffbolznv%20OR%20author%3Amarty1885%20OR%20author%3A0cc4m%20OR%20author%3ATitaniumtown%20OR%20author%3Aangt%20OR%20author%3AIMbackK%20OR%20author%3Aarthw%20OR%20author%3AJohannesGaessler%20OR%20author%3AORippler%20OR%20author%3Aruixiang63%20OR%20author%3Axctan%20OR%20author%3Aallozaur%20OR%20author%3Ayomaytk%20OR%20author%3Aaendk%20OR%20author%3Agaugarg-nv%20OR%20author%3Ataronaeo%20OR%20author%3Aforforever73%20OR%20author%3Alhez%20OR%20author%3Anetrunnereve%20OR%20author%3Afairydreaming)%20sort%3Aupdated-desc) / [dev stats](https://github.com/ggml-org/llama.cpp-dev) / [lib llama API](https://github.com/ggml-org/llama.cpp/issues/9289) / [llama-server REST API](https://github.com/ggml-org/llama.cpp/issues/9291)
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -307,10 +307,19 @@ function gg_run_test_llama_archs_tensor_split {
|
||||
|
||||
set -e
|
||||
|
||||
GGML_CUDA_DEVICES=1 ./build-ci-release/bin/test-llama-archs -s 1 2>&1
|
||||
GGML_CUDA_DEVICES=2 ./build-ci-release/bin/test-llama-archs -s 1 2>&1
|
||||
GGML_CUDA_DEVICES=3 ./build-ci-release/bin/test-llama-archs -s 1 2>&1
|
||||
GGML_CUDA_DEVICES=4 ./build-ci-release/bin/test-llama-archs -s 1 2>&1
|
||||
if [ ! -z ${GG_BUILD_CUDA} ]; then
|
||||
GGML_CUDA_DEVICES=1 ./build-ci-release/bin/test-llama-archs -s 1 2>&1
|
||||
GGML_CUDA_DEVICES=2 ./build-ci-release/bin/test-llama-archs -s 1 2>&1
|
||||
GGML_CUDA_DEVICES=3 ./build-ci-release/bin/test-llama-archs -s 1 2>&1
|
||||
GGML_CUDA_DEVICES=4 ./build-ci-release/bin/test-llama-archs -s 1 2>&1
|
||||
fi
|
||||
|
||||
if [ ! -z ${GG_BUILD_METAL} ]; then
|
||||
GGML_METAL_DEVICES=1 ./build-ci-release/bin/test-llama-archs -s 1 2>&1
|
||||
GGML_METAL_DEVICES=2 ./build-ci-release/bin/test-llama-archs -s 1 2>&1
|
||||
GGML_METAL_DEVICES=3 ./build-ci-release/bin/test-llama-archs -s 1 2>&1
|
||||
GGML_METAL_DEVICES=4 ./build-ci-release/bin/test-llama-archs -s 1 2>&1
|
||||
fi
|
||||
|
||||
set +e
|
||||
}
|
||||
@@ -318,7 +327,7 @@ function gg_run_test_llama_archs_tensor_split {
|
||||
function gg_sum_test_llama_archs_tensor_split {
|
||||
gg_printf '### %s\n\n' "${ci}"
|
||||
|
||||
gg_printf 'Runs test-llama-archs with 1 to 4 CUDA devices\n'
|
||||
gg_printf 'Runs test-llama-archs with 1 to 4 devices\n'
|
||||
gg_printf '- status: %s\n' "$(cat $OUT/${ci}.exit)"
|
||||
gg_printf '```\n'
|
||||
gg_printf '%s\n' "$(cat $OUT/${ci}.log)"
|
||||
@@ -776,9 +785,7 @@ ret=0
|
||||
test $ret -eq 0 && gg_run ctest_debug
|
||||
test $ret -eq 0 && gg_run ctest_release
|
||||
|
||||
if [ ! -z ${GG_BUILD_CUDA} ]; then
|
||||
test $ret -eq 0 && gg_run test_llama_archs_tensor_split
|
||||
fi
|
||||
test $ret -eq 0 && gg_run test_llama_archs_tensor_split
|
||||
|
||||
if [ ! -z ${GG_BUILD_HIGH_PERF} ]; then
|
||||
test $ret -eq 0 && gg_run test_backend_ops_cpu
|
||||
|
||||
+3
-2
@@ -402,10 +402,11 @@ void common_params_print_info(const common_params & params, bool print_devices)
|
||||
#endif
|
||||
COM_TRC("%s: build %d (%s) with %s for %s%s\n", __func__, llama_build_number(), llama_commit(), llama_compiler(), llama_build_target(), build_type);
|
||||
|
||||
COM_INF("%s: verbosity = %d (adjust with the `-lv N` CLI arg)\n", __func__, common_log_get_verbosity_thold());
|
||||
const int verbosity = common_log_get_verbosity_thold();
|
||||
COM_INF("%s: verbosity = %d (adjust with the `-lv N` CLI arg)\n", __func__, verbosity);
|
||||
|
||||
// device enumeration creates a primary context on CUDA backends, skip it when the caller does not own any device
|
||||
if (print_devices) {
|
||||
if (print_devices && verbosity >= LOG_LEVEL_TRACE) {
|
||||
COM_TRC("%s", "device_info:\n");
|
||||
for (size_t i = 0; i < ggml_backend_dev_count(); ++i) {
|
||||
auto * dev = ggml_backend_dev_get(i);
|
||||
|
||||
+42
-5
@@ -112,12 +112,36 @@ class GlmOCRModel(Glm4Model):
|
||||
@ModelBase.example("zai-org/GLM-4.5-Air")
|
||||
class Glm4MoeModel(TextModel):
|
||||
model_arch = gguf.MODEL_ARCH.GLM4_MOE
|
||||
supports_mtp_export = True
|
||||
_n_main_layers: int | None = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# GLM4_MOE has num_hidden_layers + 1 actual layers (including NextN layer)
|
||||
self.block_count = self.hparams["num_hidden_layers"] + self.hparams.get("num_nextn_predict_layers", 0)
|
||||
self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count)
|
||||
if not self.no_mtp:
|
||||
self.block_count += self.hparams.get("num_nextn_predict_layers", 0)
|
||||
self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count)
|
||||
|
||||
def index_tensors(self, remote_hf_model_id: str | None = None):
|
||||
type(self)._n_main_layers = self.hparams["num_hidden_layers"]
|
||||
return super().index_tensors(remote_hf_model_id=remote_hf_model_id)
|
||||
|
||||
@classmethod
|
||||
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
||||
if (titem := super().filter_tensors(item)) is None:
|
||||
return None
|
||||
name, gen = titem
|
||||
|
||||
assert cls._n_main_layers is not None
|
||||
is_mtp = (m := re.match(r"model\.layers\.(\d+)\.", name)) is not None and int(m.group(1)) >= cls._n_main_layers
|
||||
|
||||
if is_mtp and cls.no_mtp:
|
||||
return None
|
||||
if cls.mtp_only and not is_mtp and name not in (
|
||||
"model.embed_tokens.weight", "model.norm.weight", "lm_head.weight",
|
||||
):
|
||||
return None
|
||||
|
||||
return name, gen
|
||||
|
||||
def set_vocab(self):
|
||||
return self._set_vocab_glm()
|
||||
@@ -153,10 +177,22 @@ class Glm4MoeModel(TextModel):
|
||||
if (norm_topk_prob := self.hparams.get("norm_topk_prob")) is not None:
|
||||
self.gguf_writer.add_expert_weights_norm(norm_topk_prob)
|
||||
|
||||
# NextN/MTP prediction layers
|
||||
if (num_nextn_predict_layers := self.hparams.get("num_nextn_predict_layers")) is not None:
|
||||
if not self.no_mtp and (num_nextn_predict_layers := self.hparams.get("num_nextn_predict_layers")) is not None:
|
||||
self.gguf_writer.add_nextn_predict_layers(num_nextn_predict_layers)
|
||||
|
||||
def prepare_metadata(self, vocab_only: bool):
|
||||
from_dir = self.fname_out.is_dir()
|
||||
super().prepare_metadata(vocab_only=vocab_only)
|
||||
|
||||
if not self.mtp_only or not from_dir:
|
||||
return
|
||||
|
||||
output_type: str = self.ftype.name.partition("_")[2]
|
||||
fname_default: str = gguf.naming_convention(
|
||||
self.metadata.name, self.metadata.basename, self.metadata.finetune,
|
||||
self.metadata.version, size_label=None, output_type=output_type, model_type=None)
|
||||
self.fname_out = self.fname_out.parent / f"mtp-{fname_default}.gguf"
|
||||
|
||||
_experts: list[dict[str, Tensor]] | None = None
|
||||
|
||||
# note: unlike GLM4V non-MoE, we don't need to permute Q/K here since GLM4V_MOE uses Neox ordering already
|
||||
@@ -348,6 +384,7 @@ class GlmMoeDsaModel(DeepseekV2Model):
|
||||
@ModelBase.example("upstage/Solar-Open-100B")
|
||||
class SolarOpenModel(Glm4MoeModel):
|
||||
model_arch = gguf.MODEL_ARCH.GLM4_MOE
|
||||
supports_mtp_export = False
|
||||
|
||||
def set_vocab(self):
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
+7
-7
@@ -443,21 +443,21 @@ Each returned parser is wrapped by `wrap_for_generation_prompt()`, which prepend
|
||||
| | `wrap_for_generation_prompt()`, string helpers |
|
||||
| `common/chat-peg-parser.h/cpp` | `common_chat_peg_builder`, `common_chat_peg_mapper`, and helpers |
|
||||
| `common/chat.cpp` | Entry point: `common_chat_templates_apply_jinja()` |
|
||||
| `tools/parser/debug-template-parser.cpp` | Debug tool for template analysis |
|
||||
| `tools/parser/template-analysis.cpp` | Template analysis tool |
|
||||
| `tests/test-chat-auto-parser.cpp` | Auto-parser unit tests; also a debug tool when given a template path |
|
||||
| `tests/test-chat-analysis.cpp` | Template differential analysis debug tool |
|
||||
|
||||
## Testing & Debugging
|
||||
|
||||
### Debug Tools
|
||||
|
||||
**Template Debugger**: `tools/parser/debug-template-parser.cpp`
|
||||
**Template Debugger**: `tests/test-chat-auto-parser.cpp`
|
||||
|
||||
- Usage: `./bin/llama-debug-template-parser path/to/template.jinja`
|
||||
- Usage: `./bin/test-chat-auto-parser path/to/template.jinja` (without a path, it runs the automated tests)
|
||||
- Shows detected format, markers, generated parser, and GBNF grammar
|
||||
|
||||
**Template Analysis**: `tools/parser/template-analysis.cpp`
|
||||
**Template Analysis**: `tests/test-chat-analysis.cpp`
|
||||
|
||||
- Usage: `./bin/llama-template-analysis path/to/template.jinja`
|
||||
- Usage: `./bin/test-chat-analysis --template-file path/to/template.jinja` (without arguments, it runs on all templates from the test suite)
|
||||
|
||||
**Debug Logging**: Enable with `LLAMA_ARG_LOG_VERBOSITY=2`
|
||||
|
||||
@@ -519,7 +519,7 @@ The following templates have active tests in `tests/test-chat.cpp`:
|
||||
|
||||
To support a new template format:
|
||||
|
||||
1. **If it follows standard patterns** — The auto-parser should detect it automatically. Run `llama-debug-template-parser` to verify markers are correctly extracted.
|
||||
1. **If it follows standard patterns** — The auto-parser should detect it automatically. Run `test-chat-auto-parser <template_path>` to verify markers are correctly extracted.
|
||||
2. **If differential analysis extracts incorrect markers** — Add a workaround lambda to the `workarounds` vector in `common/chat-diff-analyzer.cpp`. Inspect the template source for a unique identifying substring.
|
||||
3. **If it needs fundamentally different handling** — Add a dedicated handler function in `chat.cpp` before the auto-parser block (as done for GPT-OSS, Functionary v3.2, and Ministral).
|
||||
|
||||
|
||||
@@ -3822,7 +3822,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
|
||||
MODEL_TENSOR.FFN_DOWN_SHEXP,
|
||||
MODEL_TENSOR.FFN_UP_SHEXP,
|
||||
MODEL_TENSOR.FFN_EXP_PROBS_B,
|
||||
# NextN/MTP tensors - preserved but unused
|
||||
# NextN/MTP tensors
|
||||
MODEL_TENSOR.NEXTN_EH_PROJ,
|
||||
MODEL_TENSOR.NEXTN_EMBED_TOKENS,
|
||||
MODEL_TENSOR.NEXTN_ENORM,
|
||||
|
||||
@@ -66,7 +66,7 @@ These recur often enough in review comments on past add-model PRs that they're w
|
||||
- Optional hparams that are genuinely absent from some configs (e.g. a shared-expert count) should be read with an explicit optional/fallback accessor, not assumed present.
|
||||
- Hparams that are actually load-bearing (the model produces wrong output or crashes without them, e.g. `sliding_window_pattern`, norm-eps) must hard-error if missing, not silently fall back to a default.
|
||||
- Don't bake a default chat template into the C++ binary - inject it into the GGUF at conversion time instead, since one `llm_arch` can be reused by multiple fine-tunes with different templates, and a baked-in C++ default fails silently for those.
|
||||
- Before writing a dedicated tool-call/output parser, check whether the existing autoparser already handles the template (`llama-debug-template-parser <jinja>` shows what it detects).
|
||||
- Before writing a dedicated tool-call/output parser, check whether the existing autoparser already handles the template (`test-chat-auto-parser <jinja>` shows what it detects).
|
||||
- Marking a custom EOS/closing-tag token as `eot` at conversion time isn't always sufficient - in long/agentic generations a model can emit the closing sequence as literal text instead of the token, so generation never stops on EOG and raw text leaks past the parser. Verify this case, not just the token path.
|
||||
- If reusing or aliasing an existing pre-tokenizer for convenience, justify and test that choice explicitly - silent reuse is an easy source of subtle tokenizer bugs.
|
||||
- Watch for excessive graph splits caused by building per-layer view/index tensors inside the layer loop - hoist tensors that don't vary per layer out of the loop (relevant if you hit `GGML_SCHED_MAX_SPLIT_INPUTS`).
|
||||
|
||||
+186
-16
@@ -29,10 +29,19 @@ void llama_model_glm4_moe::load_arch_hparams(llama_model_loader & ml) {
|
||||
}
|
||||
}
|
||||
|
||||
void llama_model_glm4_moe::load_arch_tensors(llama_model_loader &) {
|
||||
void llama_model_glm4_moe::load_arch_tensors(llama_model_loader & ml) {
|
||||
LLAMA_LOAD_LOCALS;
|
||||
const int64_t n_expert_shared = hparams.n_expert_shared;
|
||||
|
||||
const bool mtp_only = (hparams.n_layer_nextn > 0) && (ml.get_weight("blk.0.attn_norm.weight") == nullptr);
|
||||
const std::string mtp_probe = "blk." + std::to_string(n_layer) + ".nextn.eh_proj.weight";
|
||||
const bool trunk_only = (hparams.n_layer_nextn > 0) && (ml.get_weight(mtp_probe.c_str()) == nullptr);
|
||||
const int trunk_flags = mtp_only ? TENSOR_NOT_REQUIRED : 0;
|
||||
int mtp_flags = trunk_only ? TENSOR_NOT_REQUIRED : 0;
|
||||
|
||||
if (!ml.load_mtp) {
|
||||
mtp_flags |= TENSOR_SKIP;
|
||||
}
|
||||
|
||||
GGML_ASSERT(hparams.n_expert > 0 && "n_expert must be > 0 for GLM4_MOE MoE layers");
|
||||
GGML_ASSERT(hparams.n_expert_used > 0 && "n_expert_used must be > 0 for GLM4_MOE MoE layers");
|
||||
@@ -47,16 +56,9 @@ void llama_model_glm4_moe::load_arch_tensors(llama_model_loader &) {
|
||||
output = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), { n_embd, n_vocab }, TENSOR_DUPLICATED);
|
||||
}
|
||||
|
||||
// Load ALL tensors including NextN layer to satisfy total tensor count
|
||||
// but only PROCESS up to last layer (skipping final NextN layer) in forward pass
|
||||
for (int i = 0; i < n_layer_all; ++i) {
|
||||
int flags = 0;
|
||||
if (i >= n_layer) {
|
||||
// skip all tensors in the NextN layers
|
||||
flags |= TENSOR_SKIP;
|
||||
}
|
||||
|
||||
auto & layer = layers[i];
|
||||
const int flags = i < n_layer ? trunk_flags : mtp_flags;
|
||||
|
||||
layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i), { n_embd }, flags);
|
||||
|
||||
@@ -110,24 +112,186 @@ void llama_model_glm4_moe::load_arch_tensors(llama_model_loader &) {
|
||||
layer.ffn_up = create_tensor(tn(LLM_TENSOR_FFN_UP, "weight", i), { n_embd, n_ff }, flags);
|
||||
}
|
||||
|
||||
// NextN/MTP tensors (preserved but unused) - conditionally load for last nextn_predict_layers
|
||||
// NextN/MTP tensors
|
||||
if (i >= n_layer) {
|
||||
layer.nextn.eh_proj = create_tensor(tn(LLM_TENSOR_NEXTN_EH_PROJ, "weight", i), { 2 * n_embd, n_embd }, flags);
|
||||
layer.nextn.enorm = create_tensor(tn(LLM_TENSOR_NEXTN_ENORM, "weight", i), { n_embd }, flags);
|
||||
layer.nextn.hnorm = create_tensor(tn(LLM_TENSOR_NEXTN_HNORM, "weight", i), { n_embd }, flags);
|
||||
|
||||
// Optional tensors
|
||||
layer.nextn.embed_tokens = create_tensor(tn(LLM_TENSOR_NEXTN_EMBED_TOKENS, "weight", i), { n_embd, n_vocab }, flags | TENSOR_NOT_REQUIRED);
|
||||
layer.nextn.shared_head_head = create_tensor(tn(LLM_TENSOR_NEXTN_SHARED_HEAD_HEAD, "weight", i), { n_embd, n_vocab }, flags | TENSOR_NOT_REQUIRED);
|
||||
layer.nextn.shared_head_norm = create_tensor(tn(LLM_TENSOR_NEXTN_SHARED_HEAD_NORM, "weight", i), { n_embd }, flags | TENSOR_NOT_REQUIRED);
|
||||
layer.nextn.embed_tokens = create_tensor(tn(LLM_TENSOR_NEXTN_EMBED_TOKENS, "weight", i), { n_embd, n_vocab }, TENSOR_NOT_REQUIRED | flags);
|
||||
layer.nextn.shared_head_head = create_tensor(tn(LLM_TENSOR_NEXTN_SHARED_HEAD_HEAD, "weight", i), { n_embd, n_vocab }, TENSOR_NOT_REQUIRED | flags);
|
||||
layer.nextn.shared_head_norm = create_tensor(tn(LLM_TENSOR_NEXTN_SHARED_HEAD_NORM, "weight", i), { n_embd }, TENSOR_NOT_REQUIRED | flags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<llm_graph_context> llama_model_glm4_moe::build_arch_graph(const llm_graph_params & params) const {
|
||||
if (params.gtype == LLM_GRAPH_TYPE_DECODER_MTP) {
|
||||
return std::make_unique<graph_mtp>(*this, params);
|
||||
}
|
||||
return std::make_unique<graph>(*this, params);
|
||||
}
|
||||
|
||||
llama_model_glm4_moe::graph_mtp::graph_mtp(const llama_model & model, const llm_graph_params & params)
|
||||
: llm_graph_context(params) {
|
||||
GGML_ASSERT(hparams.n_layer_nextn > 0 && "GLM4_MOE MTP requires n_layer_nextn > 0");
|
||||
GGML_ASSERT(hparams.n_layer_nextn == 1 && "GLM4_MOE MTP currently only supports a single MTP block");
|
||||
|
||||
const int64_t n_embd_head = hparams.n_embd_head_v();
|
||||
GGML_ASSERT(n_embd_head == hparams.n_embd_head_k());
|
||||
|
||||
const int il = hparams.n_layer() + cparams.nextn_layer_offset;
|
||||
GGML_ASSERT(cparams.nextn_layer_offset >= 0 &&
|
||||
cparams.nextn_layer_offset < (int) hparams.n_layer_nextn &&
|
||||
"nextn_layer_offset out of range [0, n_layer_nextn)");
|
||||
|
||||
const auto & layer = model.layers[il];
|
||||
|
||||
GGML_ASSERT(layer.nextn.eh_proj && "MTP block missing nextn.eh_proj");
|
||||
GGML_ASSERT(layer.nextn.enorm && "MTP block missing nextn.enorm");
|
||||
GGML_ASSERT(layer.nextn.hnorm && "MTP block missing nextn.hnorm");
|
||||
GGML_ASSERT(layer.ffn_gate_inp && "MTP block missing ffn_gate_inp");
|
||||
|
||||
auto inp = std::make_unique<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 = build_attn_inp_kv();
|
||||
|
||||
ggml_tensor * h_norm = build_norm(h_embd, layer.nextn.hnorm, nullptr, LLM_NORM_RMS, il);
|
||||
cb(h_norm, "mtp_hnorm", il);
|
||||
|
||||
ggml_tensor * e_norm = build_norm(tok_embd, layer.nextn.enorm, nullptr, LLM_NORM_RMS, il);
|
||||
cb(e_norm, "mtp_enorm", il);
|
||||
|
||||
ggml_tensor * concat = ggml_concat(ctx0, e_norm, h_norm, 0);
|
||||
cb(concat, "mtp_concat", il);
|
||||
|
||||
ggml_tensor * cur = build_lora_mm(layer.nextn.eh_proj, concat, layer.nextn.eh_proj_s);
|
||||
cb(cur, "mtp_eh_proj", il);
|
||||
|
||||
ggml_tensor * inpSA = cur;
|
||||
|
||||
cur = build_norm(cur, layer.attn_norm, nullptr, LLM_NORM_RMS, il);
|
||||
cb(cur, "mtp_attn_norm", il);
|
||||
|
||||
auto [Qcur, Kcur, Vcur] = build_qkv(layer, cur,
|
||||
n_embd_head, n_head, n_head_kv, il);
|
||||
|
||||
if (layer.attn_q_norm) {
|
||||
Qcur = build_norm(Qcur, layer.attn_q_norm, nullptr, LLM_NORM_RMS, il);
|
||||
cb(Qcur, "mtp_Qcur_normed", il);
|
||||
}
|
||||
if (layer.attn_k_norm) {
|
||||
Kcur = build_norm(Kcur, layer.attn_k_norm, nullptr, LLM_NORM_RMS, il);
|
||||
cb(Kcur, "mtp_Kcur_normed", il);
|
||||
}
|
||||
|
||||
Qcur = ggml_rope_ext(ctx0, Qcur, inp_pos, nullptr, n_rot,
|
||||
rope_type, n_ctx_orig, freq_base, freq_scale,
|
||||
ext_factor, attn_factor, beta_fast, beta_slow);
|
||||
|
||||
Kcur = ggml_rope_ext(ctx0, Kcur, inp_pos, nullptr, n_rot,
|
||||
rope_type, n_ctx_orig, freq_base, freq_scale,
|
||||
ext_factor, attn_factor, beta_fast, beta_slow);
|
||||
|
||||
cb(Qcur, "mtp_Qcur", il);
|
||||
cb(Kcur, "mtp_Kcur", il);
|
||||
cb(Vcur, "mtp_Vcur", il);
|
||||
|
||||
cur = build_attn(inp_attn,
|
||||
layer.wo, nullptr, layer.wo_s,
|
||||
Qcur, Kcur, Vcur, nullptr, nullptr, nullptr,
|
||||
1.0f / sqrtf(float(n_embd_head)), il);
|
||||
cb(cur, "mtp_attn_out", il);
|
||||
|
||||
ggml_tensor * ffn_inp = ggml_add(ctx0, cur, inpSA);
|
||||
cb(ffn_inp, "mtp_ffn_inp", il);
|
||||
|
||||
cur = build_norm(ffn_inp, layer.attn_post_norm, nullptr, LLM_NORM_RMS, il);
|
||||
cb(cur, "mtp_post_attn_norm", il);
|
||||
|
||||
ggml_tensor * routed_out = build_moe_ffn(cur,
|
||||
layer.ffn_gate_inp,
|
||||
layer.ffn_up_exps,
|
||||
layer.ffn_gate_exps,
|
||||
layer.ffn_down_exps,
|
||||
layer.ffn_exp_probs_b,
|
||||
n_expert, n_expert_used,
|
||||
LLM_FFN_SILU, hparams.expert_weights_norm,
|
||||
hparams.expert_weights_scale,
|
||||
(llama_expert_gating_func_type) hparams.expert_gating_func,
|
||||
il);
|
||||
cb(routed_out, "mtp_ffn_moe_out", il);
|
||||
|
||||
ggml_tensor * shared_out = build_ffn(cur,
|
||||
layer.ffn_up_shexp, nullptr, nullptr,
|
||||
layer.ffn_gate_shexp, nullptr, nullptr,
|
||||
layer.ffn_down_shexp, nullptr, nullptr,
|
||||
nullptr,
|
||||
LLM_FFN_SILU, LLM_FFN_PAR, il);
|
||||
cb(shared_out, "mtp_ffn_shexp_out", il);
|
||||
|
||||
cur = ggml_add(ctx0, routed_out, shared_out);
|
||||
cb(cur, "mtp_ffn_out", il);
|
||||
|
||||
cur = ggml_add(ctx0, cur, ffn_inp);
|
||||
cb(cur, "mtp_post_ffn", il);
|
||||
|
||||
ggml_tensor * head_norm_w = layer.nextn.shared_head_norm
|
||||
? layer.nextn.shared_head_norm
|
||||
: model.output_norm;
|
||||
GGML_ASSERT(head_norm_w && "GLM4_MOE MTP: missing both nextn.shared_head_norm and output_norm");
|
||||
|
||||
cur = build_norm(cur, head_norm_w, nullptr, LLM_NORM_RMS, -1);
|
||||
cb(cur, "h_nextn", -1);
|
||||
res->t_h_nextn = cur;
|
||||
|
||||
if (inp_out_ids) {
|
||||
cur = ggml_get_rows(ctx0, cur, inp_out_ids);
|
||||
}
|
||||
cb(cur, "mtp_shared_head_norm", -1);
|
||||
|
||||
ggml_tensor * head_w = layer.nextn.shared_head_head
|
||||
? layer.nextn.shared_head_head
|
||||
: model.output;
|
||||
ggml_tensor * head_s = layer.nextn.shared_head_head
|
||||
? layer.nextn.shared_head_head_s
|
||||
: model.output_s;
|
||||
GGML_ASSERT(head_w && "GLM4_MOE MTP: missing LM head (nextn.shared_head_head or model.output)");
|
||||
|
||||
cur = build_lora_mm(head_w, cur, head_s);
|
||||
cb(cur, "result_output", -1);
|
||||
|
||||
res->t_logits = cur;
|
||||
ggml_build_forward_expand(gf, cur);
|
||||
}
|
||||
|
||||
llama_model_glm4_moe::graph::graph(const llama_model & model, const llm_graph_params & params) : llm_graph_context(params) {
|
||||
const int64_t n_embd_head = hparams.n_embd_head_v();
|
||||
|
||||
@@ -154,8 +318,7 @@ llama_model_glm4_moe::graph::graph(const llama_model & model, const llm_graph_pa
|
||||
|
||||
ggml_tensor * inp_out_ids = build_inp_out_ids();
|
||||
|
||||
// Only process up to last layer (skip final NextN layer)
|
||||
// Final layer tensors are loaded but not processed in forward pass
|
||||
// NextN layers are processed by graph_mtp.
|
||||
for (int il = 0; il < n_layer; ++il) {
|
||||
ggml_tensor * inpSA = inpL;
|
||||
|
||||
@@ -205,7 +368,7 @@ llama_model_glm4_moe::graph::graph(const llama_model & model, const llm_graph_pa
|
||||
model.layers[il].wo, NULL, model.layers[il].wo_s,
|
||||
Qcur, Kcur, Vcur, nullptr, nullptr, nullptr, 1.0f/sqrtf(float(n_embd_head)), il);
|
||||
}
|
||||
if (il == n_layer - 1 && inp_out_ids) {
|
||||
if (il == n_layer - 1 && inp_out_ids && (!cparams.embeddings_nextn || cparams.embeddings_nextn_masked)) {
|
||||
cur = ggml_get_rows(ctx0, cur, inp_out_ids);
|
||||
inpSA = ggml_get_rows(ctx0, inpSA, inp_out_ids);
|
||||
}
|
||||
@@ -265,6 +428,13 @@ llama_model_glm4_moe::graph::graph(const llama_model & model, const llm_graph_pa
|
||||
cur = inpL;
|
||||
cur = build_norm(cur, model.output_norm, NULL, LLM_NORM_RMS, -1);
|
||||
|
||||
cb(cur, "h_nextn", -1);
|
||||
res->t_h_nextn = cur;
|
||||
|
||||
if (cparams.embeddings_nextn && !cparams.embeddings_nextn_masked && inp_out_ids) {
|
||||
cur = ggml_get_rows(ctx0, cur, inp_out_ids);
|
||||
}
|
||||
|
||||
cb(cur, "result_norm", -1);
|
||||
res->t_embd = cur;
|
||||
|
||||
|
||||
@@ -1412,6 +1412,10 @@ struct llama_model_glm4_moe : public llama_model_base {
|
||||
graph(const llama_model & model, const llm_graph_params & params);
|
||||
};
|
||||
|
||||
struct graph_mtp : public llm_graph_context {
|
||||
graph_mtp(const llama_model & model, const llm_graph_params & params);
|
||||
};
|
||||
|
||||
std::unique_ptr<llm_graph_context> build_arch_graph(const llm_graph_params & params) const override;
|
||||
};
|
||||
|
||||
|
||||
@@ -244,6 +244,8 @@ llama_build_and_test(test-jinja.cpp)
|
||||
llama_test(test-jinja NAME test-jinja-py ARGS -py LABEL python)
|
||||
llama_build_and_test(test-chat-auto-parser.cpp WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})
|
||||
llama_build_and_test(test-chat-template.cpp)
|
||||
# debug tool for chat template differential analysis (not registered as a test, run it manually)
|
||||
llama_build(test-chat-analysis.cpp)
|
||||
llama_build_and_test(test-log.cpp)
|
||||
llama_build_and_test(
|
||||
test-peg-parser.cpp
|
||||
|
||||
@@ -84,11 +84,12 @@ static std::string read_file(const std::string & path) {
|
||||
}
|
||||
|
||||
static void print_usage(const char * program_name) {
|
||||
LOG_ERR("Usage: %s [options]\n", program_name);
|
||||
LOG_ERR("Debug the auto-parser's differential analysis: render a template with/without tools, reasoning, etc. and show the diffs.\n");
|
||||
LOG_ERR("\nUsage: %s [options]\n", program_name);
|
||||
LOG_ERR("\nOptions:\n");
|
||||
LOG_ERR(" --template <name> Analyze specific template from test suite (e.g., 'deepseek' or 'DeepSeek-V3.1')\n");
|
||||
LOG_ERR(" --template-file <path> Analyze custom template file\n");
|
||||
LOG_ERR(" --all Analyze all templates from test suite\n");
|
||||
LOG_ERR(" --all Analyze all templates from test suite (default when no arguments are given)\n");
|
||||
LOG_ERR("\nExamples:\n");
|
||||
LOG_ERR(" %s --all\n", program_name);
|
||||
LOG_ERR(" %s --template deepseek\n", program_name);
|
||||
@@ -97,14 +98,17 @@ static void print_usage(const char * program_name) {
|
||||
|
||||
static bool parse_options(int argc, char ** argv, analysis_options & opts) {
|
||||
if (argc < 2) {
|
||||
print_usage(argv[0]);
|
||||
return false;
|
||||
// default mode: analyze all templates from the test suite
|
||||
opts.analyze_all = true;
|
||||
}
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string arg = argv[i];
|
||||
|
||||
if (arg == "--all") {
|
||||
if (arg == "-h" || arg == "--help") {
|
||||
print_usage(argv[0]);
|
||||
return false;
|
||||
} else if (arg == "--all") {
|
||||
opts.analyze_all = true;
|
||||
} else if (arg == "--template") {
|
||||
if (i + 1 >= argc) {
|
||||
@@ -2,11 +2,18 @@
|
||||
#include "chat-auto-parser.h"
|
||||
#include "chat-peg-parser.h"
|
||||
#include "chat.h"
|
||||
#include "gguf.h"
|
||||
#include "jinja/runtime.h"
|
||||
#include "log.h"
|
||||
#include "peg-parser.h"
|
||||
#include "testing.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
@@ -94,11 +101,447 @@ static void test_bailing_v3_tool_format(testing & t);
|
||||
|
||||
static void test_role_markers_all_templates(testing & t);
|
||||
|
||||
static json build_tools_definition();
|
||||
|
||||
//
|
||||
// debug mode: analyze a single template and dump the generated parser and grammar
|
||||
//
|
||||
|
||||
enum class output_mode {
|
||||
ANALYSIS, // Only output analysis results (default)
|
||||
TEMPLATE, // Only output rendered template
|
||||
BOTH // Output both
|
||||
};
|
||||
|
||||
enum class input_message_type {
|
||||
NONE, // Don't render any message scenarios (only analysis)
|
||||
CONTENT_ONLY, // Simple assistant message with content
|
||||
REASONING_CONTENT, // Message with reasoning_content + content
|
||||
TOOL_CALL_ONLY, // Message with tool_calls only
|
||||
CONTENT_TOOL_CALL, // Message with content + tool_calls
|
||||
REASONING_TOOL_CALL, // Message with reasoning_content + tool_calls
|
||||
CONTENT_FAKE_TOOL_CALL, // Message with content but no actual tool_calls (for testing)
|
||||
ALL // Render all scenarios
|
||||
};
|
||||
|
||||
struct debug_options {
|
||||
std::string template_path;
|
||||
bool with_tools = true;
|
||||
bool generation_prompt = true;
|
||||
bool enable_reasoning = true;
|
||||
bool debug_jinja = false;
|
||||
bool force_tool_call = false;
|
||||
bool parallel_tool_calls = true;
|
||||
output_mode mode = output_mode::BOTH;
|
||||
input_message_type input_message = input_message_type::NONE;
|
||||
};
|
||||
|
||||
static std::string read_file(const std::string & path) {
|
||||
std::ifstream fin(path, std::ios::binary);
|
||||
if (!fin.is_open()) {
|
||||
throw std::runtime_error("Could not open file: " + path);
|
||||
}
|
||||
std::ostringstream buf;
|
||||
buf << fin.rdbuf();
|
||||
return buf.str();
|
||||
}
|
||||
|
||||
static std::string read_gguf_chat_template(const std::string & path) {
|
||||
struct gguf_init_params params = { /*no_alloc =*/true, // We only need metadata, not tensor data
|
||||
/*ctx=*/nullptr };
|
||||
|
||||
struct gguf_context * ctx = gguf_init_from_file(path.c_str(), params);
|
||||
if (ctx == nullptr) {
|
||||
throw std::runtime_error("Could not open GGUF file: " + path);
|
||||
}
|
||||
|
||||
const char * key = "tokenizer.chat_template";
|
||||
int64_t key_id = gguf_find_key(ctx, key);
|
||||
|
||||
if (key_id == -1) {
|
||||
gguf_free(ctx);
|
||||
throw std::runtime_error("GGUF file does not contain chat template key: " + std::string(key));
|
||||
}
|
||||
|
||||
const char * template_str = gguf_get_val_str(ctx, key_id);
|
||||
if (template_str == nullptr) {
|
||||
gguf_free(ctx);
|
||||
throw std::runtime_error("GGUF file contains chat template key but value is null");
|
||||
}
|
||||
|
||||
std::string result = template_str;
|
||||
gguf_free(ctx);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void print_usage(const char * program_name) {
|
||||
LOG_ERR("Test the chat template auto-parser; also usable as a debug tool that shows the generated PEG parser, GBNF grammar and triggers for a given template.\n");
|
||||
LOG_ERR("\nUsage: %s [filter_regex] run the automated tests (default)\n", program_name);
|
||||
LOG_ERR(" %s <template_or_gguf_path> [options] debug a single template\n", program_name);
|
||||
LOG_ERR("\nDebug mode options:\n");
|
||||
LOG_ERR(" --no-tools Disable tool definitions\n");
|
||||
LOG_ERR(" --force-tool-call Set tool calls to forced\n");
|
||||
LOG_ERR(" --parallel-tool-calls=0|1 Set parallel_tool_calls (default: 1)\n");
|
||||
LOG_ERR(" --generation-prompt=0|1 Set add_generation_prompt (default: 1)\n");
|
||||
LOG_ERR(" --enable-reasoning=0|1 Enable reasoning parsing (default: 1)\n");
|
||||
LOG_ERR(" --output=MODE Output mode: analysis, template, both (default: both)\n");
|
||||
LOG_ERR(" --debug-jinja Enable Jinja fine-grained debug\n");
|
||||
LOG_ERR(" --input-message=TYPE Message type to render:\n");
|
||||
LOG_ERR(" content_only, reasoning_content, tool_call_only,\n");
|
||||
LOG_ERR(" content_tool_call, reasoning_tool_call,\n");
|
||||
LOG_ERR(" content_fake_tool_call, all\n");
|
||||
LOG_ERR("\nExamples:\n");
|
||||
LOG_ERR(" %s template.jinja --input-message=all --generation-prompt=1\n", program_name);
|
||||
LOG_ERR(" %s template.jinja --output=template --input-message=tool_call_only\n", program_name);
|
||||
}
|
||||
|
||||
static bool parse_bool_option(const std::string & value) {
|
||||
return value == "1" || value == "true" || value == "yes";
|
||||
}
|
||||
|
||||
static bool parse_debug_options(int argc, char ** argv, debug_options & opts) {
|
||||
opts.template_path = argv[1];
|
||||
|
||||
for (int i = 2; i < argc; ++i) {
|
||||
std::string arg = argv[i];
|
||||
|
||||
if (arg == "--force-tool-call") {
|
||||
opts.force_tool_call = true;
|
||||
} else if (arg == "--debug-jinja") {
|
||||
opts.debug_jinja = true;
|
||||
} else if (arg == "--no-tools") {
|
||||
opts.with_tools = false;
|
||||
} else if (arg.rfind("--parallel-tool-calls=", 0) == 0) {
|
||||
opts.parallel_tool_calls = parse_bool_option(arg.substr(22));
|
||||
} else if (arg.rfind("--generation-prompt=", 0) == 0) {
|
||||
opts.generation_prompt = parse_bool_option(arg.substr(20));
|
||||
} else if (arg.rfind("--enable-reasoning=", 0) == 0) {
|
||||
opts.enable_reasoning = parse_bool_option(arg.substr(19));
|
||||
} else if (arg.rfind("--output=", 0) == 0) {
|
||||
std::string mode = arg.substr(9);
|
||||
if (mode == "analysis") {
|
||||
opts.mode = output_mode::ANALYSIS;
|
||||
} else if (mode == "template") {
|
||||
opts.mode = output_mode::TEMPLATE;
|
||||
} else if (mode == "both") {
|
||||
opts.mode = output_mode::BOTH;
|
||||
} else {
|
||||
LOG_ERR("Unknown output mode: %s\n", mode.c_str());
|
||||
return false;
|
||||
}
|
||||
} else if (arg.rfind("--input-message=", 0) == 0) {
|
||||
std::string type = arg.substr(16);
|
||||
if (type == "content_only") {
|
||||
opts.input_message = input_message_type::CONTENT_ONLY;
|
||||
} else if (type == "reasoning_content") {
|
||||
opts.input_message = input_message_type::REASONING_CONTENT;
|
||||
} else if (type == "tool_call_only") {
|
||||
opts.input_message = input_message_type::TOOL_CALL_ONLY;
|
||||
} else if (type == "content_tool_call") {
|
||||
opts.input_message = input_message_type::CONTENT_TOOL_CALL;
|
||||
} else if (type == "reasoning_tool_call") {
|
||||
opts.input_message = input_message_type::REASONING_TOOL_CALL;
|
||||
} else if (type == "content_fake_tool_call") {
|
||||
opts.input_message = input_message_type::CONTENT_FAKE_TOOL_CALL;
|
||||
} else if (type == "all") {
|
||||
opts.input_message = input_message_type::ALL;
|
||||
} else {
|
||||
LOG_ERR("Unknown input message type: %s\n", type.c_str());
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
LOG_ERR("Unknown option: %s\n", arg.c_str());
|
||||
print_usage(argv[0]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static json build_debug_user_message() {
|
||||
return json{
|
||||
{ "role", "user" },
|
||||
{ "content", "Hello, please help me with a task." }
|
||||
};
|
||||
}
|
||||
|
||||
static json build_content_only_message() {
|
||||
return json{
|
||||
{ "role", "assistant" },
|
||||
{ "content", "Hello! I'm here to help you with your task." }
|
||||
};
|
||||
}
|
||||
|
||||
static json build_reasoning_content_message() {
|
||||
return json{
|
||||
{ "role", "assistant" },
|
||||
{ "content", "Hello! I'm here to help you with your task." },
|
||||
{ "reasoning_content", "The user is greeting me and asking for help. I should respond politely." }
|
||||
};
|
||||
}
|
||||
|
||||
static json build_tool_call_only_message() {
|
||||
return json{
|
||||
{ "role", "assistant" },
|
||||
{ "content", nullptr },
|
||||
{ "tool_calls",
|
||||
json::array({ json{
|
||||
{ "type", "function" },
|
||||
{ "function", json{ { "name", "test_function_name" },
|
||||
{ "arguments", json::object({ { "param1", "value1" }, { "param2", "value2" } }) } } },
|
||||
{ "id", "123456789" } } }) }
|
||||
};
|
||||
}
|
||||
|
||||
static json build_content_tool_call_message() {
|
||||
return json{
|
||||
{ "role", "assistant" },
|
||||
{ "content", "I'll help you by calling a function." },
|
||||
{ "tool_calls",
|
||||
json::array({ json{
|
||||
{ "type", "function" },
|
||||
{ "function",
|
||||
json{ { "name", "test_function_name" },
|
||||
{ "arguments", json::object({ { "param1", "value1" }, { "param2", "value2" } }) } } } } }) }
|
||||
};
|
||||
}
|
||||
|
||||
static json build_reasoning_tool_call_message() {
|
||||
return json{
|
||||
{ "role", "assistant" },
|
||||
{ "content", nullptr },
|
||||
{ "reasoning_content", "I need to call a function to help with this task." },
|
||||
{ "tool_calls",
|
||||
json::array({ json{
|
||||
{ "type", "function" },
|
||||
{ "function",
|
||||
json{ { "name", "test_function_name" },
|
||||
{ "arguments", json::object({ { "param1", "value1" }, { "param2", "value2" } }) } } } } }) }
|
||||
};
|
||||
}
|
||||
|
||||
static json build_content_fake_tool_call_message() {
|
||||
// This message has content but NO tool_calls field
|
||||
// It's used to test if a template renders tool definitions but not tool calls
|
||||
return json{
|
||||
{ "role", "assistant" },
|
||||
{ "content", "I'll help you by calling a function." }
|
||||
};
|
||||
}
|
||||
|
||||
static void render_scenario(const common_chat_template & tmpl,
|
||||
const std::string & scenario_name,
|
||||
const json & messages,
|
||||
const json & tools,
|
||||
bool add_generation_prompt,
|
||||
bool enable_thinking) {
|
||||
LOG_ERR("\n=== Scenario: %s ===\n", scenario_name.c_str());
|
||||
LOG_ERR("add_generation_prompt: %s, enable_thinking: %s\n", add_generation_prompt ? "true" : "false",
|
||||
enable_thinking ? "true" : "false");
|
||||
|
||||
// When add_generation_prompt is true, add a trailing user message to trigger the prompt
|
||||
json final_messages = messages;
|
||||
if (add_generation_prompt && !messages.empty() && messages.back().value("role", "") == "assistant") {
|
||||
final_messages.push_back(json{
|
||||
{ "role", "user" },
|
||||
{ "content", "Now please continue with another response." }
|
||||
});
|
||||
}
|
||||
|
||||
LOG_ERR("Messages:\n%s\n", final_messages.dump(2).c_str());
|
||||
|
||||
try {
|
||||
generation_params inputs;
|
||||
inputs.messages = final_messages;
|
||||
inputs.add_generation_prompt = add_generation_prompt;
|
||||
inputs.extra_context["enable_thinking"] = enable_thinking;
|
||||
|
||||
if (!tools.is_null() && tools.is_array() && !tools.empty()) {
|
||||
inputs.tools = tools;
|
||||
}
|
||||
|
||||
std::string output = common_chat_template_direct_apply(tmpl, inputs);
|
||||
|
||||
LOG_ERR("\n--- Rendered Output ---\n");
|
||||
LOG_ERR("%s\n", output.c_str());
|
||||
LOG_ERR("--- End Output (length: %zu) ---\n", output.length());
|
||||
} catch (const std::exception & e) {
|
||||
LOG_ERR("Rendering failed: %s\n", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
static void render_all_scenarios(const common_chat_template & tmpl,
|
||||
const json & tools,
|
||||
bool add_generation_prompt,
|
||||
bool enable_thinking,
|
||||
input_message_type message_type) {
|
||||
json user_msg = build_debug_user_message();
|
||||
|
||||
auto render_if = [&](input_message_type type, const std::string & name, const json & assistant_msg) {
|
||||
if (message_type == input_message_type::ALL || message_type == type) {
|
||||
json messages = json::array({ user_msg, assistant_msg });
|
||||
render_scenario(tmpl, name, messages, tools, add_generation_prompt, enable_thinking);
|
||||
}
|
||||
};
|
||||
|
||||
render_if(input_message_type::CONTENT_ONLY, "content_only", build_content_only_message());
|
||||
render_if(input_message_type::REASONING_CONTENT, "reasoning_content", build_reasoning_content_message());
|
||||
render_if(input_message_type::TOOL_CALL_ONLY, "tool_call_only", build_tool_call_only_message());
|
||||
render_if(input_message_type::CONTENT_TOOL_CALL, "content_tool_call", build_content_tool_call_message());
|
||||
render_if(input_message_type::REASONING_TOOL_CALL, "reasoning_tool_call", build_reasoning_tool_call_message());
|
||||
render_if(input_message_type::CONTENT_FAKE_TOOL_CALL, "content_fake_tool_call",
|
||||
build_content_fake_tool_call_message());
|
||||
|
||||
// Also render with add_generation_prompt=true to show the prompt ending
|
||||
if (message_type == input_message_type::ALL) {
|
||||
LOG_ERR("\n\n=== Generation Prompt Scenarios (add_generation_prompt=true) ===\n");
|
||||
|
||||
json prompt_messages = json::array({ user_msg });
|
||||
render_scenario(tmpl, "generation_prompt_only", prompt_messages, tools, true, enable_thinking);
|
||||
|
||||
// With enable_thinking toggled
|
||||
render_scenario(tmpl, "generation_prompt_thinking_disabled", prompt_messages, tools, true, false);
|
||||
}
|
||||
}
|
||||
|
||||
static generation_params prepare_debug_params(const debug_options & opts, const json & tools) {
|
||||
generation_params params;
|
||||
params.messages = json::array({ build_debug_user_message() });
|
||||
params.reasoning_format = opts.enable_reasoning ? COMMON_REASONING_FORMAT_DEEPSEEK : COMMON_REASONING_FORMAT_NONE;
|
||||
params.enable_thinking = opts.enable_reasoning;
|
||||
params.add_generation_prompt = opts.generation_prompt;
|
||||
|
||||
if (opts.with_tools) {
|
||||
params.tools = tools;
|
||||
params.tool_choice = opts.force_tool_call ? COMMON_CHAT_TOOL_CHOICE_REQUIRED : COMMON_CHAT_TOOL_CHOICE_AUTO;
|
||||
} else {
|
||||
params.tools = json();
|
||||
params.tool_choice = COMMON_CHAT_TOOL_CHOICE_NONE;
|
||||
}
|
||||
params.parallel_tool_calls = opts.parallel_tool_calls;
|
||||
return params;
|
||||
}
|
||||
|
||||
static int debug_single_template(const debug_options & opts) {
|
||||
std::string template_source;
|
||||
try {
|
||||
// Check if the file is a GGUF file
|
||||
if (opts.template_path.size() >= 5 &&
|
||||
opts.template_path.compare(opts.template_path.size() - 5, 5, ".gguf") == 0) {
|
||||
template_source = read_gguf_chat_template(opts.template_path);
|
||||
} else {
|
||||
template_source = read_file(opts.template_path);
|
||||
}
|
||||
} catch (const std::exception & e) {
|
||||
LOG_ERR("Error reading template: %s\n", e.what());
|
||||
return 1;
|
||||
}
|
||||
|
||||
LOG_ERR("Analyzing template: %s\n", opts.template_path.c_str());
|
||||
LOG_ERR("Options: with_tools=%s, generation_prompt=%s, enable_reasoning=%s\n", opts.with_tools ? "true" : "false",
|
||||
opts.generation_prompt ? "true" : "false", opts.enable_reasoning ? "true" : "false");
|
||||
|
||||
try {
|
||||
common_chat_template chat_template(template_source, "", "");
|
||||
|
||||
json tools = opts.with_tools ? build_tools_definition() : json();
|
||||
|
||||
generation_params params = prepare_debug_params(opts, tools);
|
||||
common_chat_params parser_data;
|
||||
if (std::optional<common_chat_params> spec_tmpl =
|
||||
common_chat_try_specialized_template(chat_template, template_source, params)) {
|
||||
LOG_ERR("\n");
|
||||
LOG_ERR("This template uses a specialized parser, analysis results will not be available.\n");
|
||||
parser_data = *spec_tmpl;
|
||||
} else {
|
||||
// Render template scenarios if requested
|
||||
if (opts.input_message != input_message_type::NONE &&
|
||||
(opts.mode == output_mode::TEMPLATE || opts.mode == output_mode::BOTH)) {
|
||||
LOG_ERR("\n");
|
||||
LOG_ERR("================================================================================\n");
|
||||
LOG_ERR(" TEMPLATE RENDERING OUTPUT\n");
|
||||
LOG_ERR("================================================================================\n");
|
||||
|
||||
render_all_scenarios(chat_template, tools, opts.generation_prompt, opts.enable_reasoning,
|
||||
opts.input_message);
|
||||
}
|
||||
|
||||
// Output analysis if requested
|
||||
if (opts.mode == output_mode::ANALYSIS || opts.mode == output_mode::BOTH) {
|
||||
LOG_ERR("\n");
|
||||
LOG_ERR("================================================================================\n");
|
||||
LOG_ERR(" TEMPLATE ANALYSIS\n");
|
||||
LOG_ERR("================================================================================\n");
|
||||
|
||||
struct autoparser analysis;
|
||||
analysis.analyze_template(chat_template);
|
||||
|
||||
// Generate Parser
|
||||
parser_data = peg_generator::generate_parser(chat_template, params, analysis);
|
||||
}
|
||||
}
|
||||
|
||||
if (!std::empty(parser_data.parser)) {
|
||||
LOG_ERR("\n=== Generated Parser ===\n");
|
||||
common_peg_arena arena;
|
||||
arena.load(parser_data.parser);
|
||||
LOG_ERR("%s\n", arena.dump(arena.root()).c_str());
|
||||
|
||||
LOG_ERR("\n=== Generated Grammar ===\n");
|
||||
LOG_ERR("%s\n", parser_data.grammar.c_str());
|
||||
|
||||
LOG_ERR("\n=== Generated Lazy Grammar ===\n");
|
||||
LOG_ERR("%d\n", parser_data.grammar_lazy);
|
||||
|
||||
LOG_ERR("\n=== Generated Grammar Triggers ===\n");
|
||||
for (const common_grammar_trigger & cgt : parser_data.grammar_triggers) {
|
||||
LOG_ERR("Token: %d | Type: %d | Value: %s\n", cgt.token, cgt.type, cgt.value.c_str());
|
||||
}
|
||||
|
||||
LOG_ERR("\n=== Preserved Tokens ===\n");
|
||||
for (const std::string & token : parser_data.preserved_tokens) {
|
||||
LOG_ERR(" '%s'\n", token.c_str());
|
||||
}
|
||||
}
|
||||
} catch (const std::exception & e) {
|
||||
LOG_ERR("Analysis failed: %s\n", e.what());
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char * argv[]) {
|
||||
if (argc > 1) {
|
||||
std::string arg = argv[1];
|
||||
if (arg == "-h" || arg == "--help") {
|
||||
common_log_set_verbosity_thold(99);
|
||||
print_usage(argv[0]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// debug mode: if the first argument is an existing file, analyze that template instead of running the automated tests
|
||||
if (std::filesystem::is_regular_file(arg)) {
|
||||
common_log_set_verbosity_thold(99);
|
||||
|
||||
debug_options opts;
|
||||
if (!parse_debug_options(argc, argv, opts)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (opts.debug_jinja || std::getenv("LLAMA_DEBUG_JINJA") != nullptr) {
|
||||
jinja::enable_debug(true);
|
||||
}
|
||||
|
||||
return debug_single_template(opts);
|
||||
}
|
||||
}
|
||||
|
||||
testing t(std::cout);
|
||||
t.verbose = true;
|
||||
|
||||
// usage: test-chat-auto-parser-helpers [filter_regex]
|
||||
// usage: test-chat-auto-parser [filter_regex]
|
||||
|
||||
if (argc > 1) {
|
||||
t.set_filter(argv[1]);
|
||||
|
||||
@@ -28,6 +28,8 @@ static void run_multiple(const std::string& dir_path, bool stop_on_first_failure
|
||||
static void run_single(const std::string& contents, json input, bool use_common = false, bool dump_prog = false, const std::string & output_path = "");
|
||||
|
||||
static std::string HELP = R"(
|
||||
Test the Jinja engine by rendering chat templates and comparing the output against expected results.
|
||||
|
||||
Usage: test-chat-template [OPTIONS] PATH_TO_TEMPLATE
|
||||
Options:
|
||||
-h, --help Show this help message and exit.
|
||||
|
||||
@@ -27,7 +27,6 @@ else()
|
||||
add_subdirectory(server)
|
||||
endif()
|
||||
add_subdirectory(tokenize)
|
||||
add_subdirectory(parser)
|
||||
add_subdirectory(tts)
|
||||
add_subdirectory(mtmd)
|
||||
if (GGML_RPC)
|
||||
|
||||
@@ -29,10 +29,10 @@ enum patch_merge_type {
|
||||
PATCH_MERGE_SPATIAL_UNPAD,
|
||||
};
|
||||
|
||||
// all algos are Pillow-compatible (matching PIL.Image.resize output)
|
||||
enum resize_algo {
|
||||
RESIZE_ALGO_BILINEAR, // stretch to target resolution
|
||||
RESIZE_ALGO_BICUBIC, // center-crop when aspect ratio doesn't match
|
||||
RESIZE_ALGO_BICUBIC_PILLOW,
|
||||
RESIZE_ALGO_BILINEAR,
|
||||
RESIZE_ALGO_BICUBIC,
|
||||
RESIZE_ALGO_LANCZOS,
|
||||
};
|
||||
|
||||
@@ -73,7 +73,7 @@ struct clip_hparams {
|
||||
int32_t preproc_max_tiles = 0;
|
||||
int32_t preproc_tile_size = 0; // local tile size (deepseek-ocr)
|
||||
resize_algo image_resize_algo_rf = RESIZE_ALGO_BICUBIC;
|
||||
resize_algo image_resize_algo_ov = RESIZE_ALGO_BILINEAR;
|
||||
resize_algo image_resize_algo_ov = RESIZE_ALGO_BICUBIC;
|
||||
pad_style image_pad_rf = PAD_CEIL; // padding style for the refined image (e.g. llava-1.6)
|
||||
pad_style image_pad_ov = PAD_NONE; // padding style for the overview image (e.g. llava-1.6)
|
||||
std::array<uint8_t, 3> image_pad_color_rf = {0, 0, 0}; // padding color for refined image
|
||||
|
||||
+20
-19
@@ -1420,20 +1420,18 @@ struct clip_model_loader {
|
||||
hparams.image_pad_color = {122, 116, 104};
|
||||
if (!hparams.image_res_candidates.empty()) {
|
||||
hparams.image_resize_pad = PAD_CEIL;
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BILINEAR;
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC;
|
||||
} else {
|
||||
// llava-1.6 default params
|
||||
hparams.image_pad_ov = PAD_NONE;
|
||||
hparams.image_pad_rf = PAD_CEIL;
|
||||
hparams.image_pad_color_rf = {122, 116, 104};
|
||||
hparams.image_resize_algo_rf = RESIZE_ALGO_BICUBIC;
|
||||
hparams.image_resize_algo_ov = RESIZE_ALGO_BILINEAR;
|
||||
}
|
||||
} break;
|
||||
case PROJECTOR_TYPE_GLM_EDGE:
|
||||
{
|
||||
hparams.image_resize_pad = PAD_CEIL;
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BILINEAR;
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC;
|
||||
} break;
|
||||
case PROJECTOR_TYPE_MINICPMV:
|
||||
{
|
||||
@@ -1490,6 +1488,7 @@ struct clip_model_loader {
|
||||
case PROJECTOR_TYPE_IDEFICS3:
|
||||
{
|
||||
// use default llava-uhd preprocessing params
|
||||
hparams.image_resize_algo = RESIZE_ALGO_LANCZOS;
|
||||
get_u32(KEY_PROJ_SCALE_FACTOR, hparams.n_merge, false);
|
||||
get_u32(KEY_PREPROC_IMAGE_SIZE, hparams.image_longest_edge, false);
|
||||
hparams.set_limit_image_tokens();
|
||||
@@ -1516,7 +1515,7 @@ struct clip_model_loader {
|
||||
// ref: https://huggingface.co/mistral-community/pixtral-12b/blob/main/preprocessor_config.json
|
||||
// TODO: verify the image_min_tokens
|
||||
hparams.n_merge = 1; // the original pixtral does not use patch merging
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BILINEAR;
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC;
|
||||
hparams.rope_theta = 10000.0f;
|
||||
get_u32(KEY_SPATIAL_MERGE_SIZE, hparams.n_merge, false);
|
||||
hparams.set_limit_image_tokens(8, 1024);
|
||||
@@ -1544,7 +1543,7 @@ struct clip_model_loader {
|
||||
case PROJECTOR_TYPE_DOTS3NOTE_V:
|
||||
{
|
||||
hparams.rope_theta = 10000.0f;
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC_PILLOW;
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC;
|
||||
get_u32(KEY_SPATIAL_MERGE_SIZE, hparams.n_merge);
|
||||
get_u32(KEY_IMAGE_MIN_PIXELS, hparams.image_min_pixels);
|
||||
get_u32(KEY_IMAGE_MAX_PIXELS, hparams.image_max_pixels);
|
||||
@@ -1562,7 +1561,7 @@ struct clip_model_loader {
|
||||
} break;
|
||||
case PROJECTOR_TYPE_KIMIVL:
|
||||
{
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BILINEAR;
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC;
|
||||
hparams.rope_theta = 10000.0f;
|
||||
get_u32(KEY_PROJ_SCALE_FACTOR, hparams.n_merge, false);
|
||||
// TODO: check kimivl preprocessor for exact values
|
||||
@@ -1601,7 +1600,7 @@ struct clip_model_loader {
|
||||
{
|
||||
hparams.rope_theta = 100.0f;
|
||||
hparams.n_merge = 3; // pooling_kernel_size
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BILINEAR;
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC;
|
||||
get_u32(KEY_PROJ_SCALE_FACTOR, hparams.n_merge, false);
|
||||
if (model.proj_type == PROJECTOR_TYPE_GEMMA4UV) {
|
||||
// for "unified" variant, we directly use a bigger patch size, because the "token merging" is done directly on conv layer
|
||||
@@ -1618,6 +1617,7 @@ struct clip_model_loader {
|
||||
// Gemma3n uses MobileNetV5 which produces 256 tokens (16x16)
|
||||
// Similar configuration to Gemma3
|
||||
hparams.n_merge = 1; // MobileNetV5 handles resizing internally
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BILINEAR;
|
||||
get_u32(KEY_PROJ_SCALE_FACTOR, hparams.n_merge, false);
|
||||
} break;
|
||||
case PROJECTOR_TYPE_QWEN2VL:
|
||||
@@ -1625,7 +1625,7 @@ struct clip_model_loader {
|
||||
case PROJECTOR_TYPE_QWEN3VL:
|
||||
{
|
||||
hparams.n_merge = 2; // default value for Qwen 2 and 2.5
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BILINEAR;
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC;
|
||||
get_u32(KEY_SPATIAL_MERGE_SIZE, hparams.n_merge, false);
|
||||
get_u32(KEY_WIN_ATTN_PATTERN, hparams.n_wa_pattern, model.proj_type == PROJECTOR_TYPE_QWEN25VL); // only 2.5 requires it
|
||||
// ref: https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct/blob/main/preprocessor_config.json
|
||||
@@ -1641,7 +1641,7 @@ struct clip_model_loader {
|
||||
case PROJECTOR_TYPE_MINIMAX_M3:
|
||||
{
|
||||
hparams.n_merge = 2; // spatial_merge_size
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC_PILLOW;
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC;
|
||||
hparams.image_resize_pad = PAD_NONE;
|
||||
get_u32(KEY_SPATIAL_MERGE_SIZE, hparams.n_merge, false);
|
||||
// n_merge is used as a divisor in clip_image_batch_encode
|
||||
@@ -1666,7 +1666,7 @@ struct clip_model_loader {
|
||||
case PROJECTOR_TYPE_MIMOVL:
|
||||
{
|
||||
hparams.n_merge = 2; // spatial_merge_size
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC_PILLOW;
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC;
|
||||
get_u32(KEY_SPATIAL_MERGE_SIZE, hparams.n_merge, false);
|
||||
get_u32(string_format(KEY_N_HEAD_KV, "vision"), hparams.n_head_kv);
|
||||
// 1D banded sliding-window radius (visual_token_window_size); required
|
||||
@@ -1713,15 +1713,15 @@ struct clip_model_loader {
|
||||
log_ffn_op = "gelu_erf";
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC;
|
||||
|
||||
// reka model performs better when using resize_bicubic, which stretches
|
||||
// the image to fit fixed square size
|
||||
// reka model performs better when the image is stretched to fit
|
||||
// fixed square size (no padding)
|
||||
hparams.image_resize_pad = PAD_NONE;
|
||||
} break;
|
||||
case PROJECTOR_TYPE_GLM4V:
|
||||
{
|
||||
hparams.rope_theta = 10000.0f;
|
||||
hparams.n_merge = 2; // default value for GLM4-V
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BILINEAR;
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC;
|
||||
get_u32(KEY_SPATIAL_MERGE_SIZE, hparams.n_merge, false);
|
||||
hparams.set_limit_image_tokens(8, 4096);
|
||||
hparams.set_warmup_n_tokens(46*46); // avoid OOM on warmup
|
||||
@@ -1729,6 +1729,7 @@ struct clip_model_loader {
|
||||
case PROJECTOR_TYPE_LLAMA4:
|
||||
{
|
||||
hparams.rope_theta = 10000.0f;
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BILINEAR;
|
||||
get_u32(KEY_PROJ_SCALE_FACTOR, hparams.n_merge, false);
|
||||
set_llava_uhd_res_candidates(model, 3);
|
||||
} break;
|
||||
@@ -1840,7 +1841,7 @@ struct clip_model_loader {
|
||||
case PROJECTOR_TYPE_PADDLEOCR:
|
||||
{
|
||||
hparams.n_merge = 2;
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BILINEAR;
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC;
|
||||
get_u32(KEY_IMAGE_MIN_PIXELS, hparams.image_min_pixels);
|
||||
get_u32(KEY_IMAGE_MAX_PIXELS, hparams.image_max_pixels);
|
||||
|
||||
@@ -1852,7 +1853,7 @@ struct clip_model_loader {
|
||||
hparams.patch_size = 16;
|
||||
hparams.image_size = 1024;
|
||||
hparams.warmup_image_size = 1024;
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC_PILLOW;
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC;
|
||||
hparams.image_pad_color = {127, 127, 127};
|
||||
|
||||
get_u32(KEY_SAM_N_BLOCK, hparams.sam_n_layer, true);
|
||||
@@ -1882,7 +1883,7 @@ struct clip_model_loader {
|
||||
case PROJECTOR_TYPE_HUNYUANVL:
|
||||
{
|
||||
hparams.n_merge = 2;
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC_PILLOW;
|
||||
hparams.image_resize_algo = RESIZE_ALGO_LANCZOS;
|
||||
hparams.image_resize_pad = PAD_NONE;
|
||||
hparams.ffn_op = FFN_GELU;
|
||||
hparams.set_limit_image_tokens(256, 16384);
|
||||
@@ -1955,12 +1956,12 @@ struct clip_model_loader {
|
||||
case PROJECTOR_TYPE_JANUS_PRO:
|
||||
{
|
||||
hparams.image_pad_color = {127, 127, 127};
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BILINEAR;
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC;
|
||||
} break;
|
||||
case PROJECTOR_TYPE_GRANITE4_VISION:
|
||||
{
|
||||
// SigLIP tower.
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC_PILLOW;
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC;
|
||||
hparams.image_resize_pad = PAD_CEIL;
|
||||
|
||||
// NOTE: feature_layers loaded in common path as optional
|
||||
|
||||
+82
-244
@@ -58,22 +58,7 @@ struct img_tool {
|
||||
|
||||
if (padding == PAD_NONE) {
|
||||
// direct resize
|
||||
switch (algo) {
|
||||
case RESIZE_ALGO_BILINEAR:
|
||||
resize_bilinear(src, dst, target_resolution.width, target_resolution.height);
|
||||
break;
|
||||
case RESIZE_ALGO_BICUBIC:
|
||||
resize_bicubic(src, dst, target_resolution.width, target_resolution.height);
|
||||
break;
|
||||
case RESIZE_ALGO_BICUBIC_PILLOW:
|
||||
resize_bicubic_pillow(src, dst, target_resolution.width, target_resolution.height);
|
||||
break;
|
||||
case RESIZE_ALGO_LANCZOS:
|
||||
resize_lanczos_pillow(src, dst, target_resolution.width, target_resolution.height);
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error("Unsupported resize algorithm");
|
||||
}
|
||||
resize_pillow(src, dst, target_resolution.width, target_resolution.height, algo);
|
||||
} else {
|
||||
// resize with padding
|
||||
clip_image_u8 resized_image;
|
||||
@@ -90,22 +75,7 @@ struct img_tool {
|
||||
new_height = std::min(static_cast<int>(std::ceil(src.get_size().height * scale)), target_resolution.height);
|
||||
}
|
||||
|
||||
switch (algo) {
|
||||
case RESIZE_ALGO_BILINEAR:
|
||||
resize_bilinear(src, resized_image, new_width, new_height);
|
||||
break;
|
||||
case RESIZE_ALGO_BICUBIC:
|
||||
resize_bicubic(src, resized_image, new_width, new_height);
|
||||
break;
|
||||
case RESIZE_ALGO_BICUBIC_PILLOW:
|
||||
resize_bicubic_pillow(src, resized_image, new_width, new_height);
|
||||
break;
|
||||
case RESIZE_ALGO_LANCZOS:
|
||||
resize_lanczos_pillow(src, resized_image, new_width, new_height);
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error("Unsupported resize algorithm");
|
||||
}
|
||||
resize_pillow(src, resized_image, new_width, new_height, algo);
|
||||
|
||||
// fill dst with pad_color
|
||||
fill(dst, pad_color);
|
||||
@@ -224,152 +194,37 @@ struct img_tool {
|
||||
}
|
||||
|
||||
private:
|
||||
// Bilinear resize function
|
||||
static void resize_bilinear(const clip_image_u8 & src, clip_image_u8 & dst, int target_width, int target_height) {
|
||||
const auto src_size = src.get_size();
|
||||
if (src_size.width == 0 || src_size.height == 0) { dst.set_size({0, 0}, false); return; }
|
||||
if (target_width <= 0) target_width = 1;
|
||||
if (target_height <= 0) target_height = 1;
|
||||
|
||||
dst.set_size({target_width, target_height}, false);
|
||||
|
||||
if (src.is_placeholder()) {
|
||||
// no-op for placeholder image, just set the size and return
|
||||
return;
|
||||
}
|
||||
|
||||
float x_ratio = target_width > 1 ? static_cast<float>(src_size.width - 1) / (target_width - 1) : 0.0f;
|
||||
float y_ratio = target_height > 1 ? static_cast<float>(src_size.height - 1) / (target_height - 1) : 0.0f;
|
||||
|
||||
for (int y = 0; y < target_height; ++y) {
|
||||
for (int x = 0; x < target_width; ++x) {
|
||||
float px = x * x_ratio;
|
||||
float py = y * y_ratio;
|
||||
|
||||
int x0 = std::min(static_cast<int>(px), src_size.width - 1);
|
||||
int y0 = std::min(static_cast<int>(py), src_size.height - 1);
|
||||
int x1 = std::min(x0 + 1, src_size.width - 1);
|
||||
int y1 = std::min(y0 + 1, src_size.height - 1);
|
||||
|
||||
float xf = px - x0;
|
||||
float yf = py - y0;
|
||||
|
||||
const auto p00 = src.get_pixel(x0, y0);
|
||||
const auto p10 = src.get_pixel(x1, y0);
|
||||
const auto p01 = src.get_pixel(x0, y1);
|
||||
const auto p11 = src.get_pixel(x1, y1);
|
||||
|
||||
std::array<uint8_t, 3> pixel;
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
float top = lerp(static_cast<float>(p00[c]), static_cast<float>(p10[c]), xf);
|
||||
float bottom = lerp(static_cast<float>(p01[c]), static_cast<float>(p11[c]), xf);
|
||||
pixel[c] = static_cast<uint8_t>(lerp(top, bottom, yf));
|
||||
}
|
||||
dst.set_pixel(x, y, pixel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bicubic resize function
|
||||
// part of image will be cropped if the aspect ratio is different
|
||||
static void resize_bicubic(const clip_image_u8 & img, clip_image_u8 & dst, int target_width, int target_height) {
|
||||
const auto img_size = img.get_size();
|
||||
const int nx = img_size.width;
|
||||
const int ny = img_size.height;
|
||||
|
||||
dst.set_size({target_width, target_height}, false);
|
||||
|
||||
if (img.is_placeholder()) {
|
||||
// no-op for placeholder image, just set the size and return
|
||||
return;
|
||||
}
|
||||
|
||||
float Cc;
|
||||
float C[5] = {};
|
||||
float d0, d2, d3, a0, a1, a2, a3;
|
||||
int i, j, k, jj;
|
||||
int x, y;
|
||||
float dx, dy;
|
||||
float tx, ty;
|
||||
|
||||
tx = (float)nx / (float)target_width;
|
||||
ty = (float)ny / (float)target_height;
|
||||
|
||||
// Bicubic interpolation; adapted from ViT.cpp, inspired from :
|
||||
// -> https://github.com/yglukhov/bicubic-interpolation-image-processing/blob/master/libimage.c#L36
|
||||
// -> https://en.wikipedia.org/wiki/Bicubic_interpolation
|
||||
|
||||
for (i = 0; i < target_height; i++) {
|
||||
for (j = 0; j < target_width; j++) {
|
||||
x = (int)(tx * j);
|
||||
y = (int)(ty * i);
|
||||
|
||||
dx = tx * j - x;
|
||||
dy = ty * i - y;
|
||||
|
||||
std::array<uint8_t, 3> pixel;
|
||||
for (k = 0; k < 3; k++) {
|
||||
for (jj = 0; jj <= 3; jj++) {
|
||||
d0 = img.get_pixel(clip(x - 1, 0, nx - 1), clip(y - 1 + jj, 0, ny - 1))[k] - img.get_pixel(clip(x, 0, nx - 1), clip(y - 1 + jj, 0, ny - 1))[k];
|
||||
d2 = img.get_pixel(clip(x + 1, 0, nx - 1), clip(y - 1 + jj, 0, ny - 1))[k] - img.get_pixel(clip(x, 0, nx - 1), clip(y - 1 + jj, 0, ny - 1))[k];
|
||||
d3 = img.get_pixel(clip(x + 2, 0, nx - 1), clip(y - 1 + jj, 0, ny - 1))[k] - img.get_pixel(clip(x, 0, nx - 1), clip(y - 1 + jj, 0, ny - 1))[k];
|
||||
a0 = img.get_pixel(clip(x, 0, nx - 1), clip(y - 1 + jj, 0, ny - 1))[k];
|
||||
|
||||
a1 = -1.0 / 3 * d0 + d2 - 1.0 / 6 * d3;
|
||||
a2 = 1.0 / 2 * d0 + 1.0 / 2 * d2;
|
||||
a3 = -1.0 / 6 * d0 - 1.0 / 2 * d2 + 1.0 / 6 * d3;
|
||||
|
||||
C[jj] = a0 + a1 * dx + a2 * dx * dx + a3 * dx * dx * dx;
|
||||
|
||||
d0 = C[0] - C[1];
|
||||
d2 = C[2] - C[1];
|
||||
d3 = C[3] - C[1];
|
||||
a0 = C[1];
|
||||
a1 = -1.0 / 3 * d0 + d2 - 1.0 / 6 * d3;
|
||||
a2 = 1.0 / 2 * d0 + 1.0 / 2 * d2;
|
||||
a3 = -1.0 / 6 * d0 - 1.0 / 2 * d2 + 1.0 / 6 * d3;
|
||||
Cc = a0 + a1 * dy + a2 * dy * dy + a3 * dy * dy * dy;
|
||||
|
||||
const uint8_t Cc2 = std::min(std::max(std::round(Cc), 0.0f), 255.0f);
|
||||
pixel[k] = Cc2;
|
||||
}
|
||||
}
|
||||
dst.set_pixel(j, i, pixel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pillow-compatible separable resampling (Bicubic and Lanczos)
|
||||
// Pillow-compatible separable resampling (Bilinear, Bicubic and Lanczos)
|
||||
// Adapted from https://github.com/python-pillow/Pillow/blob/main/src/libImaging/Resample.c
|
||||
//
|
||||
// Key properties:
|
||||
// 1. Separable filtering: horizontal pass followed by vertical pass
|
||||
// 2. Pre-computes normalized filter coefficients for each output pixel
|
||||
// 3. Fixed-point integer arithmetic (22 fractional bits) for speed and determinism
|
||||
static bool resize_bicubic_pillow(const clip_image_u8 & img, clip_image_u8 & dst, int target_width, int target_height) {
|
||||
return resize_pillow(img, dst, target_width, target_height, /*use_lanczos=*/false);
|
||||
}
|
||||
|
||||
// Lanczos-3 (support radius 3), matches Pillow's Image.LANCZOS
|
||||
static bool resize_lanczos_pillow(const clip_image_u8 & img, clip_image_u8 & dst, int target_width, int target_height) {
|
||||
return resize_pillow(img, dst, target_width, target_height, /*use_lanczos=*/true);
|
||||
}
|
||||
|
||||
static bool resize_pillow(
|
||||
const clip_image_u8 & img,
|
||||
clip_image_u8 & dst,
|
||||
int target_width,
|
||||
int target_height,
|
||||
bool use_lanczos) {
|
||||
resize_algo algo) {
|
||||
// Fixed-point precision: 22 bits = 32 (int32_t) - 8 (uint8_t pixels) - 2 (headroom for accumulation)
|
||||
// This allows encoding fractional weights as integers: weight * 2^22
|
||||
const int PRECISION_BITS = 32 - 8 - 2;
|
||||
|
||||
// Resample filter: Lanczos-3 (support [-3, 3]) or bicubic with a = -0.5 (support [-2, 2])
|
||||
// Note: GGML/PyTorch bicubic uses a = -0.75, Pillow uses a = -0.5
|
||||
// Filter support radius
|
||||
double filter_support;
|
||||
switch (algo) {
|
||||
case RESIZE_ALGO_BILINEAR: filter_support = 1.0; break;
|
||||
case RESIZE_ALGO_BICUBIC: filter_support = 2.0; break;
|
||||
case RESIZE_ALGO_LANCZOS: filter_support = 3.0; break;
|
||||
default:
|
||||
throw std::runtime_error("Unsupported resize algorithm");
|
||||
}
|
||||
|
||||
// Returns filter weight for distance x from pixel center
|
||||
auto resample_filter = [use_lanczos](double x) -> double {
|
||||
if (use_lanczos) {
|
||||
// Note: for bicubic, Pillow uses a = -0.5 while GGML/PyTorch use a = -0.75
|
||||
auto resample_filter = [algo](double x) -> double {
|
||||
if (algo == RESIZE_ALGO_LANCZOS) {
|
||||
if (-3.0 <= x && x < 3.0) {
|
||||
auto sinc = [](double v) {
|
||||
if (v == 0.0) {
|
||||
@@ -383,10 +238,15 @@ private:
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
constexpr double a = -0.5;
|
||||
if (x < 0.0) {
|
||||
x = -x;
|
||||
}
|
||||
|
||||
if (algo == RESIZE_ALGO_BILINEAR) {
|
||||
return x < 1.0 ? 1.0 - x : 0.0;
|
||||
}
|
||||
|
||||
constexpr double a = -0.5;
|
||||
if (x < 1.0) {
|
||||
return ((a + 2.0) * x - (a + 3.0)) * x * x + 1;
|
||||
}
|
||||
@@ -396,9 +256,6 @@ private:
|
||||
return 0.0; // Zero outside [-2, 2]
|
||||
};
|
||||
|
||||
// Filter support radius: 2 for bicubic, 3 for lanczos
|
||||
const double filter_support = use_lanczos ? 3.0 : 2.0;
|
||||
|
||||
// Clipping function for 8-bit values
|
||||
auto clip8 = [](int val) -> uint8_t {
|
||||
if (val < 0) return 0;
|
||||
@@ -493,100 +350,92 @@ private:
|
||||
const double fxp_scale = std::ldexp(1.0, PRECISION_BITS); // 1.0 * 2^PRECISION_BITS
|
||||
|
||||
for (int i = 0; i < outSize * ksize; i++) {
|
||||
if (use_lanczos) {
|
||||
// Pillow adds +/- 0.5 then truncates toward zero; std::round would round twice
|
||||
const double rounded = pre_weights[i] * fxp_scale + (pre_weights[i] < 0 ? -0.5 : 0.5);
|
||||
weights[i] = static_cast<int32_t>(rounded);
|
||||
continue;
|
||||
}
|
||||
double tmp_val = pre_weights[i] * fxp_scale;
|
||||
if (pre_weights[i] < 0) {
|
||||
tmp_val -= 0.5;
|
||||
} else {
|
||||
tmp_val += 0.5;
|
||||
}
|
||||
tmp_val = std::round(tmp_val);
|
||||
tmp_val = std::clamp(tmp_val,
|
||||
static_cast<double>(std::numeric_limits<int32_t>::min()),
|
||||
static_cast<double>(std::numeric_limits<int32_t>::max()));
|
||||
weights[i] = static_cast<int32_t>(tmp_val);
|
||||
// Pillow adds +/- 0.5 then truncates toward zero; std::round would round twice
|
||||
const double rounded = pre_weights[i] * fxp_scale + (pre_weights[i] < 0 ? -0.5 : 0.5);
|
||||
weights[i] = static_cast<int32_t>(rounded);
|
||||
}
|
||||
|
||||
return ksize;
|
||||
};
|
||||
|
||||
// Horizontal resampling pass
|
||||
// Resizes width from imIn to out_nx, preserving height
|
||||
auto resample_horizontal = [&](const clip_image_u8 & imIn, clip_image_u8 & imOut,
|
||||
// Resizes width from src to out_nx, preserving height
|
||||
auto resample_horizontal = [&](const uint8_t * src, int in_nx, int in_ny,
|
||||
int out_nx,
|
||||
int ksize, const std::vector<int> & bounds, const std::vector<int32_t> & weights) {
|
||||
const int in_ny = imIn.get_size().height;
|
||||
imOut.set_size({out_nx, in_ny}, false);
|
||||
std::vector<uint8_t> out((size_t) out_nx * in_ny * 3);
|
||||
|
||||
// Process each row independently
|
||||
for (int yy = 0; yy < in_ny; yy++) {
|
||||
const uint8_t * src_row = src + (size_t) yy * in_nx * 3;
|
||||
uint8_t * dst_row = out.data() + (size_t) yy * out_nx * 3;
|
||||
|
||||
// For each output pixel in this row
|
||||
for (int xx = 0; xx < out_nx; xx++) {
|
||||
// Get the range of input pixels and filter coefficients
|
||||
int xmin = bounds[xx * 2 + 0]; // First input pixel index
|
||||
int xcnt = bounds[xx * 2 + 1]; // Number of input pixels
|
||||
const int xmin = bounds[xx * 2 + 0]; // First input pixel index
|
||||
const int xcnt = bounds[xx * 2 + 1]; // Number of input pixels
|
||||
const int32_t * k = &weights[xx * ksize];
|
||||
const uint8_t * p = src_row + (size_t) xmin * 3;
|
||||
|
||||
// Initialize accumulators for RGB channels with rounding bias (0.5 in fixed-point)
|
||||
// Accumulators for RGB channels, with rounding bias (0.5 in fixed-point)
|
||||
int32_t ss0 = 1 << (PRECISION_BITS - 1);
|
||||
int32_t ss1 = 1 << (PRECISION_BITS - 1);
|
||||
int32_t ss2 = 1 << (PRECISION_BITS - 1);
|
||||
|
||||
// Convolve: sum weighted input pixels
|
||||
for (int x = 0; x < xcnt; x++) {
|
||||
const auto src_px = imIn.get_pixel(x + xmin, yy);
|
||||
ss0 += src_px[0] * weights[xx * ksize + x]; // R channel
|
||||
ss1 += src_px[1] * weights[xx * ksize + x]; // G channel
|
||||
ss2 += src_px[2] * weights[xx * ksize + x]; // B channel
|
||||
ss0 += p[0] * k[x];
|
||||
ss1 += p[1] * k[x];
|
||||
ss2 += p[2] * k[x];
|
||||
p += 3;
|
||||
}
|
||||
|
||||
// Convert back from fixed-point (divide by 2^PRECISION_BITS) and clamp to [0,255]
|
||||
imOut.set_pixel(xx, yy, {clip8(ss0 >> PRECISION_BITS),
|
||||
clip8(ss1 >> PRECISION_BITS),
|
||||
clip8(ss2 >> PRECISION_BITS)});
|
||||
dst_row[xx * 3 + 0] = clip8(ss0 >> PRECISION_BITS);
|
||||
dst_row[xx * 3 + 1] = clip8(ss1 >> PRECISION_BITS);
|
||||
dst_row[xx * 3 + 2] = clip8(ss2 >> PRECISION_BITS);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
// Vertical resampling pass
|
||||
// Resizes height from imIn to out_ny, preserving width
|
||||
auto resample_vertical = [&](const clip_image_u8 & imIn, clip_image_u8 & imOut,
|
||||
// Resizes height from src to out_ny, preserving width
|
||||
// Accumulates whole rows at once (contiguous access, auto-vectorizes well)
|
||||
auto resample_vertical = [&](const uint8_t * src, int in_nx,
|
||||
int out_ny,
|
||||
int ksize, const std::vector<int> & bounds, const std::vector<int32_t> & weight) {
|
||||
const int in_nx = imIn.get_size().width;
|
||||
imOut.set_size({in_nx, out_ny}, false);
|
||||
const size_t row_elems = (size_t) in_nx * 3;
|
||||
std::vector<uint8_t> out(row_elems * out_ny);
|
||||
std::vector<int32_t> acc(row_elems);
|
||||
|
||||
// For each output row
|
||||
for (int yy = 0; yy < out_ny; yy++) {
|
||||
// Get the range of input rows and filter coefficients
|
||||
int ymin = bounds[yy * 2 + 0]; // First input row index
|
||||
int ycnt = bounds[yy * 2 + 1]; // Number of input rows
|
||||
const int ymin = bounds[yy * 2 + 0]; // First input row index
|
||||
const int ycnt = bounds[yy * 2 + 1]; // Number of input rows
|
||||
const int32_t * k = &weight[yy * ksize];
|
||||
|
||||
// Process each column in this output row
|
||||
for (int xx = 0; xx < in_nx; xx++) {
|
||||
// Initialize accumulators for RGB channels with rounding bias
|
||||
int32_t ss0 = 1 << (PRECISION_BITS - 1);
|
||||
int32_t ss1 = 1 << (PRECISION_BITS - 1);
|
||||
int32_t ss2 = 1 << (PRECISION_BITS - 1);
|
||||
// Rounding bias (0.5 in fixed-point)
|
||||
std::fill(acc.begin(), acc.end(), 1 << (PRECISION_BITS - 1));
|
||||
|
||||
// Convolve: sum weighted input pixels vertically
|
||||
for (int y = 0; y < ycnt; y++) {
|
||||
const auto src_px = imIn.get_pixel(xx, y + ymin);
|
||||
ss0 += src_px[0] * weight[yy * ksize + y]; // R channel
|
||||
ss1 += src_px[1] * weight[yy * ksize + y]; // G channel
|
||||
ss2 += src_px[2] * weight[yy * ksize + y]; // B channel
|
||||
// Convolve: accumulate each weighted input row
|
||||
for (int y = 0; y < ycnt; y++) {
|
||||
const uint8_t * src_row = src + (size_t) (ymin + y) * row_elems;
|
||||
const int32_t w = k[y];
|
||||
for (size_t i = 0; i < row_elems; i++) {
|
||||
acc[i] += src_row[i] * w;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert back from fixed-point and clamp to [0,255]
|
||||
imOut.set_pixel(xx, yy, {clip8(ss0 >> PRECISION_BITS),
|
||||
clip8(ss1 >> PRECISION_BITS),
|
||||
clip8(ss2 >> PRECISION_BITS)});
|
||||
// Convert back from fixed-point and clamp to [0,255]
|
||||
uint8_t * dst_row = out.data() + (size_t) yy * row_elems;
|
||||
for (size_t i = 0; i < row_elems; i++) {
|
||||
dst_row[i] = clip8(acc[i] >> PRECISION_BITS);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
// Main resampling logic using separable two-pass approach
|
||||
@@ -610,36 +459,25 @@ private:
|
||||
}
|
||||
|
||||
// Perform two-pass resampling
|
||||
const uint8_t * src = img.get_ro_buf().data();
|
||||
if (need_horizontal && need_vertical) {
|
||||
// Both horizontal and vertical
|
||||
clip_image_u8 temp;
|
||||
resample_horizontal(img, temp, target_width, ksize_horiz, bounds_horiz, weights_horiz);
|
||||
resample_vertical(temp, dst, target_height, ksize_vert, bounds_vert, weights_vert);
|
||||
auto temp = resample_horizontal(src, src_width, src_height, target_width, ksize_horiz, bounds_horiz, weights_horiz);
|
||||
dst.set_size({target_width, target_height}, false);
|
||||
dst.cpy_buf(resample_vertical(temp.data(), target_width, target_height, ksize_vert, bounds_vert, weights_vert));
|
||||
} else if (need_horizontal) {
|
||||
// Only horizontal
|
||||
resample_horizontal(img, dst, target_width, ksize_horiz, bounds_horiz, weights_horiz);
|
||||
dst.set_size({target_width, src_height}, false);
|
||||
dst.cpy_buf(resample_horizontal(src, src_width, src_height, target_width, ksize_horiz, bounds_horiz, weights_horiz));
|
||||
} else if (need_vertical) {
|
||||
// Only vertical
|
||||
resample_vertical(img, dst, target_height, ksize_vert, bounds_vert, weights_vert);
|
||||
dst.set_size({src_width, target_height}, false);
|
||||
dst.cpy_buf(resample_vertical(src, src_width, target_height, ksize_vert, bounds_vert, weights_vert));
|
||||
} else {
|
||||
// No resizing needed - direct copy
|
||||
dst.set_size(img.get_size(), img.is_placeholder());
|
||||
if (!img.is_placeholder()) {
|
||||
dst.cpy_buf(img.get_ro_buf());
|
||||
}
|
||||
dst.set_size(img.get_size(), false);
|
||||
dst.cpy_buf(img.get_ro_buf());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline int clip(int x, int lower, int upper) {
|
||||
return std::max(lower, std::min(x, upper));
|
||||
}
|
||||
|
||||
// Linear interpolation between two points
|
||||
static inline float lerp(float s, float e, float t) {
|
||||
return s + (e - s) * t;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1264,7 +1102,7 @@ mtmd_image_preproc_out mtmd_image_preprocessor_deepseekocr::preprocess(const cli
|
||||
clip_image_u8 padded;
|
||||
img_tool::resize(img, padded,
|
||||
{ base_size, base_size },
|
||||
RESIZE_ALGO_BICUBIC_PILLOW,
|
||||
RESIZE_ALGO_BICUBIC,
|
||||
PAD_NEAREST,
|
||||
hparams.image_pad_color);
|
||||
output.append_overview(hparams, padded, true);
|
||||
@@ -1280,7 +1118,7 @@ mtmd_image_preproc_out mtmd_image_preprocessor_deepseekocr::preprocess(const cli
|
||||
grid_h = grid.height;
|
||||
|
||||
clip_image_u8 refined;
|
||||
img_tool::resize(img, refined, { tile_size * grid_w, tile_size * grid_h }, RESIZE_ALGO_BICUBIC_PILLOW,
|
||||
img_tool::resize(img, refined, { tile_size * grid_w, tile_size * grid_h }, RESIZE_ALGO_BICUBIC,
|
||||
PAD_NONE);
|
||||
|
||||
for (int row = 0; row < grid_h; row++) {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
if (NOT WIN32 OR NOT BUILD_SHARED_LIBS)
|
||||
# this tool is disabled on Windows when building with shared libraries because it uses internal functions not exported with LLAMA_API
|
||||
set(TARGET llama-debug-template-parser)
|
||||
add_executable(${TARGET} debug-template-parser.cpp)
|
||||
target_link_libraries(${TARGET} PRIVATE llama-common llama ${CMAKE_THREAD_LIBS_INIT})
|
||||
target_compile_features(${TARGET} PRIVATE cxx_std_17)
|
||||
|
||||
if(LLAMA_TOOLS_INSTALL)
|
||||
install(TARGETS ${TARGET} RUNTIME)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(TARGET llama-template-analysis)
|
||||
add_executable(${TARGET} template-analysis.cpp)
|
||||
target_link_libraries(${TARGET} PRIVATE llama-common llama ${CMAKE_THREAD_LIBS_INIT})
|
||||
target_compile_features(${TARGET} PRIVATE cxx_std_17)
|
||||
|
||||
if(LLAMA_TOOLS_INSTALL)
|
||||
install(TARGETS ${TARGET} RUNTIME)
|
||||
endif()
|
||||
@@ -1,469 +0,0 @@
|
||||
#include "../src/llama-grammar.h"
|
||||
#include "chat-auto-parser.h"
|
||||
#include "chat.h"
|
||||
#include "common.h"
|
||||
#include "gguf.h"
|
||||
#include "jinja/runtime.h"
|
||||
#include "log.h"
|
||||
#include "json.h"
|
||||
#include "peg-parser.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <numeric>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
using json = common_json;
|
||||
|
||||
enum class output_mode {
|
||||
ANALYSIS, // Only output analysis results (default)
|
||||
TEMPLATE, // Only output rendered template
|
||||
BOTH // Output both
|
||||
};
|
||||
|
||||
enum class input_message_type {
|
||||
NONE, // Don't render any message scenarios (only analysis)
|
||||
CONTENT_ONLY, // Simple assistant message with content
|
||||
REASONING_CONTENT, // Message with reasoning_content + content
|
||||
TOOL_CALL_ONLY, // Message with tool_calls only
|
||||
CONTENT_TOOL_CALL, // Message with content + tool_calls
|
||||
REASONING_TOOL_CALL, // Message with reasoning_content + tool_calls
|
||||
CONTENT_FAKE_TOOL_CALL, // Message with content but no actual tool_calls (for testing)
|
||||
ALL // Render all scenarios
|
||||
};
|
||||
|
||||
struct debug_options {
|
||||
std::string template_path;
|
||||
bool with_tools = true;
|
||||
bool generation_prompt = true;
|
||||
bool enable_reasoning = true;
|
||||
bool debug_jinja = false;
|
||||
bool force_tool_call = false;
|
||||
bool parallel_tool_calls = true;
|
||||
output_mode mode = output_mode::BOTH;
|
||||
input_message_type input_message = input_message_type::NONE;
|
||||
};
|
||||
|
||||
static std::string read_file(const std::string & path) {
|
||||
std::ifstream fin(path, std::ios::binary);
|
||||
if (!fin.is_open()) {
|
||||
throw std::runtime_error("Could not open file: " + path);
|
||||
}
|
||||
std::ostringstream buf;
|
||||
buf << fin.rdbuf();
|
||||
return buf.str();
|
||||
}
|
||||
|
||||
static std::string read_gguf_chat_template(const std::string & path) {
|
||||
struct gguf_init_params params = { /*no_alloc =*/true, // We only need metadata, not tensor data
|
||||
/*ctx=*/nullptr };
|
||||
|
||||
struct gguf_context * ctx = gguf_init_from_file(path.c_str(), params);
|
||||
if (ctx == nullptr) {
|
||||
throw std::runtime_error("Could not open GGUF file: " + path);
|
||||
}
|
||||
|
||||
const char * key = "tokenizer.chat_template";
|
||||
int64_t key_id = gguf_find_key(ctx, key);
|
||||
|
||||
if (key_id == -1) {
|
||||
gguf_free(ctx);
|
||||
throw std::runtime_error("GGUF file does not contain chat template key: " + std::string(key));
|
||||
}
|
||||
|
||||
const char * template_str = gguf_get_val_str(ctx, key_id);
|
||||
if (template_str == nullptr) {
|
||||
gguf_free(ctx);
|
||||
throw std::runtime_error("GGUF file contains chat template key but value is null");
|
||||
}
|
||||
|
||||
std::string result = template_str;
|
||||
gguf_free(ctx);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void print_usage(const char * program_name) {
|
||||
LOG_ERR("Usage: %s <template_or_gguf_path> [options]\n", program_name);
|
||||
LOG_ERR("\nOptions:\n");
|
||||
LOG_ERR(" --no-tools Disable tool definitions\n");
|
||||
LOG_ERR(" --force-tool-call Set tool calls to forced\n");
|
||||
LOG_ERR(" --parallel-tool-calls=0|1 Set parallel_tool_calls (default: 1)\n");
|
||||
LOG_ERR(" --generation-prompt=0|1 Set add_generation_prompt (default: 1)\n");
|
||||
LOG_ERR(" --enable-reasoning=0|1 Enable reasoning parsing (default: 1)\n");
|
||||
LOG_ERR(" --output=MODE Output mode: analysis, template, both (default: both)\n");
|
||||
LOG_ERR(" --debug-jinja Enable Jinja fine-grained debug\n");
|
||||
LOG_ERR(" --input-message=TYPE Message type to render:\n");
|
||||
LOG_ERR(" content_only, reasoning_content, tool_call_only,\n");
|
||||
LOG_ERR(" content_tool_call, reasoning_tool_call,\n");
|
||||
LOG_ERR(" content_fake_tool_call, all\n");
|
||||
LOG_ERR("\nExamples:\n");
|
||||
LOG_ERR(" %s template.jinja --input-message=all --generation-prompt=1\n", program_name);
|
||||
LOG_ERR(" %s template.jinja --output=template --input-message=tool_call_only\n", program_name);
|
||||
}
|
||||
|
||||
static bool parse_bool_option(const std::string & value) {
|
||||
return value == "1" || value == "true" || value == "yes";
|
||||
}
|
||||
|
||||
static bool parse_options(int argc, char ** argv, debug_options & opts) {
|
||||
if (argc < 2) {
|
||||
print_usage(argv[0]);
|
||||
return false;
|
||||
}
|
||||
|
||||
opts.template_path = argv[1];
|
||||
|
||||
for (int i = 2; i < argc; ++i) {
|
||||
std::string arg = argv[i];
|
||||
|
||||
if (arg == "--force-tool-call") {
|
||||
opts.force_tool_call = true;
|
||||
} else if (arg == "--debug-jinja") {
|
||||
opts.debug_jinja = true;
|
||||
} else if (arg == "--no-tools") {
|
||||
opts.with_tools = false;
|
||||
} else if (arg.rfind("--parallel-tool-calls=", 0) == 0) {
|
||||
opts.parallel_tool_calls = parse_bool_option(arg.substr(22));
|
||||
} else if (arg.rfind("--generation-prompt=", 0) == 0) {
|
||||
opts.generation_prompt = parse_bool_option(arg.substr(20));
|
||||
} else if (arg.rfind("--enable-reasoning=", 0) == 0) {
|
||||
opts.enable_reasoning = parse_bool_option(arg.substr(19));
|
||||
} else if (arg.rfind("--output=", 0) == 0) {
|
||||
std::string mode = arg.substr(9);
|
||||
if (mode == "analysis") {
|
||||
opts.mode = output_mode::ANALYSIS;
|
||||
} else if (mode == "template") {
|
||||
opts.mode = output_mode::TEMPLATE;
|
||||
} else if (mode == "both") {
|
||||
opts.mode = output_mode::BOTH;
|
||||
} else {
|
||||
LOG_ERR("Unknown output mode: %s\n", mode.c_str());
|
||||
return false;
|
||||
}
|
||||
} else if (arg.rfind("--input-message=", 0) == 0) {
|
||||
std::string type = arg.substr(16);
|
||||
if (type == "content_only") {
|
||||
opts.input_message = input_message_type::CONTENT_ONLY;
|
||||
} else if (type == "reasoning_content") {
|
||||
opts.input_message = input_message_type::REASONING_CONTENT;
|
||||
} else if (type == "tool_call_only") {
|
||||
opts.input_message = input_message_type::TOOL_CALL_ONLY;
|
||||
} else if (type == "content_tool_call") {
|
||||
opts.input_message = input_message_type::CONTENT_TOOL_CALL;
|
||||
} else if (type == "reasoning_tool_call") {
|
||||
opts.input_message = input_message_type::REASONING_TOOL_CALL;
|
||||
} else if (type == "content_fake_tool_call") {
|
||||
opts.input_message = input_message_type::CONTENT_FAKE_TOOL_CALL;
|
||||
} else if (type == "all") {
|
||||
opts.input_message = input_message_type::ALL;
|
||||
} else {
|
||||
LOG_ERR("Unknown input message type: %s\n", type.c_str());
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
LOG_ERR("Unknown option: %s\n", arg.c_str());
|
||||
print_usage(argv[0]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static json build_user_message() {
|
||||
return json{
|
||||
{ "role", "user" },
|
||||
{ "content", "Hello, please help me with a task." }
|
||||
};
|
||||
}
|
||||
|
||||
static json build_content_only_message() {
|
||||
return json{
|
||||
{ "role", "assistant" },
|
||||
{ "content", "Hello! I'm here to help you with your task." }
|
||||
};
|
||||
}
|
||||
|
||||
static json build_reasoning_content_message() {
|
||||
return json{
|
||||
{ "role", "assistant" },
|
||||
{ "content", "Hello! I'm here to help you with your task." },
|
||||
{ "reasoning_content", "The user is greeting me and asking for help. I should respond politely." }
|
||||
};
|
||||
}
|
||||
|
||||
static json build_tool_call_only_message() {
|
||||
return json{
|
||||
{ "role", "assistant" },
|
||||
{ "content", nullptr },
|
||||
{ "tool_calls",
|
||||
json::array({ json{
|
||||
{ "type", "function" },
|
||||
{ "function", json{ { "name", "test_function_name" },
|
||||
{ "arguments", json::object({ { "param1", "value1" }, { "param2", "value2" } }) } } },
|
||||
{ "id", "123456789" } } }) }
|
||||
};
|
||||
}
|
||||
|
||||
static json build_content_tool_call_message() {
|
||||
return json{
|
||||
{ "role", "assistant" },
|
||||
{ "content", "I'll help you by calling a function." },
|
||||
{ "tool_calls",
|
||||
json::array({ json{
|
||||
{ "type", "function" },
|
||||
{ "function",
|
||||
json{ { "name", "test_function_name" },
|
||||
{ "arguments", json::object({ { "param1", "value1" }, { "param2", "value2" } }) } } } } }) }
|
||||
};
|
||||
}
|
||||
|
||||
static json build_reasoning_tool_call_message() {
|
||||
return json{
|
||||
{ "role", "assistant" },
|
||||
{ "content", nullptr },
|
||||
{ "reasoning_content", "I need to call a function to help with this task." },
|
||||
{ "tool_calls",
|
||||
json::array({ json{
|
||||
{ "type", "function" },
|
||||
{ "function",
|
||||
json{ { "name", "test_function_name" },
|
||||
{ "arguments", json::object({ { "param1", "value1" }, { "param2", "value2" } }) } } } } }) }
|
||||
};
|
||||
}
|
||||
|
||||
static json build_content_fake_tool_call_message() {
|
||||
// This message has content but NO tool_calls field
|
||||
// It's used to test if a template renders tool definitions but not tool calls
|
||||
return json{
|
||||
{ "role", "assistant" },
|
||||
{ "content", "I'll help you by calling a function." }
|
||||
};
|
||||
}
|
||||
|
||||
static json build_tools_definition() {
|
||||
json parameters_schema = json::object();
|
||||
parameters_schema["type"] = "object";
|
||||
parameters_schema["properties"] = json::object();
|
||||
parameters_schema["properties"]["param1"] = json::object({
|
||||
{ "type", "string" },
|
||||
{ "description", "First parameter" }
|
||||
});
|
||||
parameters_schema["properties"]["param2"] = json::object({
|
||||
{ "type", "string" },
|
||||
{ "description", "Second parameter" }
|
||||
});
|
||||
parameters_schema["required"] = json::array({ "param1" });
|
||||
|
||||
return json::array({
|
||||
json{ { "type", "function" },
|
||||
{ "function", json{ { "name", "test_function_name" },
|
||||
{ "description", "A test function for debugging" },
|
||||
{ "parameters", parameters_schema } } } }
|
||||
});
|
||||
}
|
||||
|
||||
static void render_scenario(const common_chat_template & tmpl,
|
||||
const std::string & scenario_name,
|
||||
const json & messages,
|
||||
const json & tools,
|
||||
bool add_generation_prompt,
|
||||
bool enable_thinking) {
|
||||
LOG_ERR("\n=== Scenario: %s ===\n", scenario_name.c_str());
|
||||
LOG_ERR("add_generation_prompt: %s, enable_thinking: %s\n", add_generation_prompt ? "true" : "false",
|
||||
enable_thinking ? "true" : "false");
|
||||
|
||||
// When add_generation_prompt is true, add a trailing user message to trigger the prompt
|
||||
json final_messages = messages;
|
||||
if (add_generation_prompt && !messages.empty() && messages.back().value("role", "") == "assistant") {
|
||||
final_messages.push_back(json{
|
||||
{ "role", "user" },
|
||||
{ "content", "Now please continue with another response." }
|
||||
});
|
||||
}
|
||||
|
||||
LOG_ERR("Messages:\n%s\n", final_messages.dump(2).c_str());
|
||||
|
||||
try {
|
||||
autoparser::generation_params inputs;
|
||||
inputs.messages = final_messages;
|
||||
inputs.add_generation_prompt = add_generation_prompt;
|
||||
inputs.extra_context["enable_thinking"] = enable_thinking;
|
||||
|
||||
if (!tools.is_null() && tools.is_array() && !tools.empty()) {
|
||||
inputs.tools = tools;
|
||||
}
|
||||
|
||||
std::string output = common_chat_template_direct_apply(tmpl, inputs);
|
||||
|
||||
LOG_ERR("\n--- Rendered Output ---\n");
|
||||
LOG_ERR("%s\n", output.c_str());
|
||||
LOG_ERR("--- End Output (length: %zu) ---\n", output.length());
|
||||
} catch (const std::exception & e) {
|
||||
LOG_ERR("Rendering failed: %s\n", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
static void render_all_scenarios(const common_chat_template & tmpl,
|
||||
const json & tools,
|
||||
bool add_generation_prompt,
|
||||
bool enable_thinking,
|
||||
input_message_type message_type) {
|
||||
json user_msg = build_user_message();
|
||||
|
||||
auto render_if = [&](input_message_type type, const std::string & name, const json & assistant_msg) {
|
||||
if (message_type == input_message_type::ALL || message_type == type) {
|
||||
json messages = json::array({ user_msg, assistant_msg });
|
||||
render_scenario(tmpl, name, messages, tools, add_generation_prompt, enable_thinking);
|
||||
}
|
||||
};
|
||||
|
||||
render_if(input_message_type::CONTENT_ONLY, "content_only", build_content_only_message());
|
||||
render_if(input_message_type::REASONING_CONTENT, "reasoning_content", build_reasoning_content_message());
|
||||
render_if(input_message_type::TOOL_CALL_ONLY, "tool_call_only", build_tool_call_only_message());
|
||||
render_if(input_message_type::CONTENT_TOOL_CALL, "content_tool_call", build_content_tool_call_message());
|
||||
render_if(input_message_type::REASONING_TOOL_CALL, "reasoning_tool_call", build_reasoning_tool_call_message());
|
||||
render_if(input_message_type::CONTENT_FAKE_TOOL_CALL, "content_fake_tool_call",
|
||||
build_content_fake_tool_call_message());
|
||||
|
||||
// Also render with add_generation_prompt=true to show the prompt ending
|
||||
if (message_type == input_message_type::ALL) {
|
||||
LOG_ERR("\n\n=== Generation Prompt Scenarios (add_generation_prompt=true) ===\n");
|
||||
|
||||
json prompt_messages = json::array({ user_msg });
|
||||
render_scenario(tmpl, "generation_prompt_only", prompt_messages, tools, true, enable_thinking);
|
||||
|
||||
// With enable_thinking toggled
|
||||
render_scenario(tmpl, "generation_prompt_thinking_disabled", prompt_messages, tools, true, false);
|
||||
}
|
||||
}
|
||||
|
||||
static autoparser::generation_params prepare_params(const debug_options & opts, const json & tools) {
|
||||
autoparser::generation_params params;
|
||||
params.messages = json::array({ build_user_message() });
|
||||
params.reasoning_format = opts.enable_reasoning ? COMMON_REASONING_FORMAT_DEEPSEEK : COMMON_REASONING_FORMAT_NONE;
|
||||
params.enable_thinking = opts.enable_reasoning;
|
||||
params.add_generation_prompt = opts.generation_prompt;
|
||||
|
||||
if (opts.with_tools) {
|
||||
params.tools = tools;
|
||||
params.tool_choice = opts.force_tool_call ? COMMON_CHAT_TOOL_CHOICE_REQUIRED : COMMON_CHAT_TOOL_CHOICE_AUTO;
|
||||
} else {
|
||||
params.tools = json();
|
||||
params.tool_choice = COMMON_CHAT_TOOL_CHOICE_NONE;
|
||||
}
|
||||
params.parallel_tool_calls = opts.parallel_tool_calls;
|
||||
return params;
|
||||
}
|
||||
|
||||
int main(int argc, char ** argv) {
|
||||
// Set log level to most verbose to capture all debug output
|
||||
common_log_set_verbosity_thold(99);
|
||||
|
||||
debug_options opts;
|
||||
if (!parse_options(argc, argv, opts)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (opts.debug_jinja || std::getenv("LLAMA_DEBUG_JINJA") != nullptr) {
|
||||
jinja::enable_debug(true);
|
||||
}
|
||||
|
||||
std::string template_source;
|
||||
try {
|
||||
// Check if the file is a GGUF file
|
||||
if (opts.template_path.size() >= 5 &&
|
||||
opts.template_path.compare(opts.template_path.size() - 5, 5, ".gguf") == 0) {
|
||||
template_source = read_gguf_chat_template(opts.template_path);
|
||||
} else {
|
||||
template_source = read_file(opts.template_path);
|
||||
}
|
||||
} catch (const std::exception & e) {
|
||||
LOG_ERR("Error reading template: %s\n", e.what());
|
||||
return 1;
|
||||
}
|
||||
|
||||
LOG_ERR("Analyzing template: %s\n", opts.template_path.c_str());
|
||||
LOG_ERR("Options: with_tools=%s, generation_prompt=%s, enable_reasoning=%s\n", opts.with_tools ? "true" : "false",
|
||||
opts.generation_prompt ? "true" : "false", opts.enable_reasoning ? "true" : "false");
|
||||
|
||||
try {
|
||||
common_chat_template chat_template(template_source, "", "");
|
||||
|
||||
json tools = opts.with_tools ? build_tools_definition() : json();
|
||||
|
||||
autoparser::generation_params params = prepare_params(opts, tools);
|
||||
common_chat_params parser_data;
|
||||
if (std::optional<common_chat_params> spec_tmpl =
|
||||
common_chat_try_specialized_template(chat_template, template_source, params)) {
|
||||
LOG_ERR("\n");
|
||||
LOG_ERR("This template uses a specialized parser, analysis results will not be available.\n");
|
||||
parser_data = *spec_tmpl;
|
||||
} else {
|
||||
// Render template scenarios if requested
|
||||
if (opts.input_message != input_message_type::NONE &&
|
||||
(opts.mode == output_mode::TEMPLATE || opts.mode == output_mode::BOTH)) {
|
||||
LOG_ERR("\n");
|
||||
LOG_ERR("================================================================================\n");
|
||||
LOG_ERR(" TEMPLATE RENDERING OUTPUT\n");
|
||||
LOG_ERR("================================================================================\n");
|
||||
|
||||
render_all_scenarios(chat_template, tools, opts.generation_prompt, opts.enable_reasoning,
|
||||
opts.input_message);
|
||||
}
|
||||
|
||||
// Output analysis if requested
|
||||
if (opts.mode == output_mode::ANALYSIS || opts.mode == output_mode::BOTH) {
|
||||
LOG_ERR("\n");
|
||||
LOG_ERR("================================================================================\n");
|
||||
LOG_ERR(" TEMPLATE ANALYSIS\n");
|
||||
LOG_ERR("================================================================================\n");
|
||||
|
||||
autoparser::autoparser analysis;
|
||||
analysis.analyze_template(chat_template);
|
||||
|
||||
// Generate Parser
|
||||
parser_data = autoparser::peg_generator::generate_parser(chat_template, params, analysis);
|
||||
}
|
||||
}
|
||||
|
||||
if (!std::empty(parser_data.parser)) {
|
||||
LOG_ERR("\n=== Generated Parser ===\n");
|
||||
common_peg_arena arena;
|
||||
arena.load(parser_data.parser);
|
||||
LOG_ERR("%s\n", arena.dump(arena.root()).c_str());
|
||||
|
||||
LOG_ERR("\n=== Generated Grammar ===\n");
|
||||
LOG_ERR("%s\n", parser_data.grammar.c_str());
|
||||
|
||||
LOG_ERR("\n=== Generated Lazy Grammar ===\n");
|
||||
LOG_ERR("%d\n", parser_data.grammar_lazy);
|
||||
|
||||
LOG_ERR("\n=== Generated Grammar Triggers ===\n");
|
||||
for (const common_grammar_trigger & cgt : parser_data.grammar_triggers) {
|
||||
LOG_ERR("Token: %d | Type: %d | Value: %s\n", cgt.token, cgt.type, cgt.value.c_str());
|
||||
}
|
||||
|
||||
LOG_ERR("\n=== Preserved Tokens ===\n");
|
||||
for (const std::string & token : parser_data.preserved_tokens) {
|
||||
LOG_ERR(" '%s'\n", token.c_str());
|
||||
}
|
||||
|
||||
if (!parser_data.grammar.empty()) {
|
||||
LOG_ERR("\n=== Verifying created grammar ===\n");
|
||||
auto * grammar = llama_grammar_init_impl(nullptr, parser_data.grammar.c_str(), "root",
|
||||
parser_data.grammar_lazy, nullptr, 0, nullptr, 0);
|
||||
if (grammar != nullptr) {
|
||||
LOG_ERR("\n=== Grammar successfully created ===\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (const std::exception & e) {
|
||||
LOG_ERR("Analysis failed: %s\n", e.what());
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -858,8 +858,10 @@ private:
|
||||
// slots / clients
|
||||
std::vector<server_slot> slots;
|
||||
|
||||
int trace = 0;
|
||||
int slots_debug = 0;
|
||||
int trace = 0; // env: LLAMA_TRACE
|
||||
int slots_debug = 0; // env: LLAMA_SERVER_SLOTS_DEBUG
|
||||
int slots_n_diff = 0; // env: LLAMA_SERVER_SLOTS_N_DIFF
|
||||
|
||||
int n_empty_consecutive = 0;
|
||||
|
||||
std::unique_ptr<server_prompt_cache> prompt_cache;
|
||||
@@ -1247,6 +1249,15 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const char * LLAMA_SERVER_SLOTS_N_DIFF = getenv("LLAMA_SERVER_SLOTS_N_DIFF");
|
||||
slots_n_diff = LLAMA_SERVER_SLOTS_N_DIFF ? atoi(LLAMA_SERVER_SLOTS_N_DIFF) : 0;
|
||||
|
||||
if (slots_n_diff) {
|
||||
SRV_WRN("LLAMA_SERVER_SLOTS_N_DIFF = %d\n", slots_n_diff);
|
||||
}
|
||||
}
|
||||
|
||||
// the update_slots() logic will always submit a maximum of n_batch or n_parallel tokens
|
||||
// note that n_batch can be > n_ctx (e.g. for non-causal attention models such as BERT where the KV cache is not used)
|
||||
{
|
||||
@@ -3179,8 +3190,8 @@ private:
|
||||
// when the prompt prefix does not match, print the tokens around the mismatch
|
||||
// this is useful for debugging prompt caching
|
||||
if (slots_debug) {
|
||||
const int np0 = std::max<int>(n_past - 4, 0);
|
||||
const int np1 = std::min<int>(n_past + 6, std::min(slot.prompt.tokens.size(), slot.task->tokens.size()));
|
||||
const int np0 = std::max<int>(n_past - slots_n_diff, 0);
|
||||
const int np1 = std::min<int>(n_past + slots_n_diff + 2, std::min(slot.prompt.tokens.size(), slot.task->tokens.size()));
|
||||
|
||||
std::stringstream ss0;
|
||||
std::stringstream ss1;
|
||||
|
||||
@@ -319,7 +319,6 @@ def test_slot_save_restore_with_two_images(mmproj_server):
|
||||
"prompt": prompt,
|
||||
})
|
||||
assert res.status_code == 200
|
||||
content = res.body["content"]
|
||||
prompt_n_full = res.body["timings"]["prompt_n"]
|
||||
assert prompt_n_full > 64
|
||||
|
||||
@@ -345,6 +344,26 @@ def test_slot_save_restore_with_two_images(mmproj_server):
|
||||
assert res.status_code == 200
|
||||
assert res.body["timings"]["cache_n"] == prompt_n_full - 1
|
||||
assert res.body["timings"]["prompt_n"] == 1
|
||||
content = res.body["content"]
|
||||
|
||||
res = server.make_request("POST", "/slots/1?action=restore", data={
|
||||
"filename": "mm_slot_two_images.bin",
|
||||
})
|
||||
assert res.status_code == 200
|
||||
assert res.body["n_restored"] == n_saved
|
||||
|
||||
res = server.make_request("POST", "/completions", data={
|
||||
"temperature": 0.0,
|
||||
"top_k": 1,
|
||||
"id_slot": 0,
|
||||
"cache_prompt": True,
|
||||
"prompt": prompt,
|
||||
})
|
||||
assert res.status_code == 200
|
||||
assert res.body["timings"]["cache_n"] == prompt_n_full - 1
|
||||
assert res.body["timings"]["prompt_n"] == 1
|
||||
content = res.body["content"]
|
||||
|
||||
assert res.body["content"] == content
|
||||
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ def test_vision_chat_completion_token_count():
|
||||
"prompt, image_data, success, re_content",
|
||||
[
|
||||
# test model is trained on CIFAR-10, but it's quite dumb due to small size
|
||||
("What is this: <__media__>\n", "IMG_BASE64_0", True, "(cat)+"),
|
||||
("What is this: <__media__>\n", "IMG_BASE64_0", True, "(cat)+|(automobile)+"),
|
||||
("What is this: <__media__>\n", "IMG_BASE64_1", True, "(frog)+"),
|
||||
("What is this: <__media__>\n", "malformed", False, None), # non-image data
|
||||
("What is this:\n", "", False, None), # empty string
|
||||
|
||||
@@ -623,7 +623,7 @@ class ServerPreset:
|
||||
server.model_hf_repo = "ggml-org/tinygemma3-GGUF:Q8_0"
|
||||
server.model_alias = "tinygemma3"
|
||||
server.n_ctx = 1024
|
||||
server.n_batch = 32
|
||||
server.n_batch = 512
|
||||
server.n_slots = 2
|
||||
server.n_predict = 4
|
||||
server.seed = 42
|
||||
|
||||
@@ -7,6 +7,8 @@ export enum KeyboardKey {
|
||||
ARROW_RIGHT = 'ArrowRight',
|
||||
ARROW_UP = 'ArrowUp',
|
||||
B_LOWER = 'b',
|
||||
BRACKET_LEFT = 'BracketLeft',
|
||||
BRACKET_RIGHT = 'BracketRight',
|
||||
D_LOWER = 'd',
|
||||
D_UPPER = 'D',
|
||||
E_UPPER = 'E',
|
||||
|
||||
@@ -86,12 +86,12 @@ export function useKeyboardShortcuts(callbacks: KeyboardShortcutsCallbacks) {
|
||||
callbacks.navigateToNextConversation?.();
|
||||
}
|
||||
|
||||
if (isCmdOrCtrl && event.shiftKey && event.key === KeyboardKey.ARROW_LEFT) {
|
||||
if (isCmdOrCtrl && event.altKey && event.shiftKey && event.code === KeyboardKey.BRACKET_LEFT) {
|
||||
event.preventDefault();
|
||||
callbacks.navigateToPrevTab?.();
|
||||
}
|
||||
|
||||
if (isCmdOrCtrl && event.shiftKey && event.key === KeyboardKey.ARROW_RIGHT) {
|
||||
if (isCmdOrCtrl && event.altKey && event.shiftKey && event.code === KeyboardKey.BRACKET_RIGHT) {
|
||||
event.preventDefault();
|
||||
callbacks.navigateToNextTab?.();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user