Skip to content

Feat/glm4 mtp recursive - #6

Merged
F1LM1 merged 0 commit into
F1LM1:glm4-moe-mtpfrom
SamuelOliveirads:feat/glm4-mtp-recursive
Dec 21, 2025
Merged

F1LM1 merged 0 commit into
F1LM1:glm4-moe-mtpfrom
SamuelOliveirads:feat/glm4-mtp-recursive

Conversation

@SamuelOliveirads

@SamuelOliveirads SamuelOliveirads commented Dec 10, 2025 •

Copy link
Copy Markdown

Note: Please be aware that this PR shows a high commit count because it was rebased against the latest master after a long period of inactivity.

Recursive MTP Drafting & Graph Optimization for GLM-4.5

This is a follow-up PR to F1LM1/llama.cpp#5, implementing major architectural improvements for Multi-Token Prediction (MTP).

Summary of Changes

  1. Multi-Token Support: Enabled recursive drafting loop (Eagle-style) to predict N tokens per step.
  2. Graph Optimization: Activated graph reuse and fixed callback handling for MTP operations, significantly reducing CPU/scheduler overhead.
  3. Rebase: Updated the branch to the latest master (including server-loop optimizations from server: improve speed of speculative decoding ggml-org/llama.cpp#17808).

Detailed Changes

1. Recursive MTP Drafting

Previously, MTP was limited to a single draft token per loop, capping the theoretical maximum performance. This PR implements a recursive generation loop based on the architecture found in sglang#8224.

Architecture:
The MTP draft is not a parallel tensor operation but a recursive loop. The draft inputs require the embedding of the current token and the fixed hidden state from the main model's last interaction.

  1. Main Model: Processes Prompt ($t_0...t_n$) $\rightarrow$ Outputs Hidden State ($H_{main}$) and Token ($t_{n+1}$).
  2. MTP Loop (Draft 1): Input ($$Emb(t_{n+1})$$) + Context ($H_{main}$) $\rightarrow$ MTP Layer $\rightarrow$ Token ($t_{n+2}$).
  3. MTP Loop (Draft 2): Input ($$Emb(t_{n+2})$$) + Context ($H_{main}$) $\rightarrow$ MTP Layer $\rightarrow$ Token ($t_{n+3}$).
  4. Verification: Validates the sequence against the main model.

Controls:
I reused common_speculative_params:

  • --draft-max N: Sets the maximum number of recursive drafts.
  • --draft-p-min P: Confidence threshold. The loop exits early if the MTP model is not confident, saving compute resources.

Recommended Test Command:

-mtp --draft-max 3 --draft-p-min 0.85

(Note: I found better results by limiting draft-max and increasing p-min to avoid low-quality speculation).

2. Rebase & Modernization

I updated the codebase to include the latest commits from master (approx. 5 months of updates). This includes the recent server-loop refactor (ggml-org#17808). While the server refactor itself didn't yield massive gains in my specific MTP tests, the overall codebase improvements combined with my MTP fixes have boosted baseline performance.

3. Graph Reuse Optimization

A major bottleneck was the lack of graph reuse during MTP switching.

  • Problem: Switching between Main Model computation (~10ms) and MTP computation (~9.6ms) forced a full graph rebuild and allocation every step.
  • Fix: Added proper cb() callbacks to the MTP tensors in build_mtp_tail. This allows the scheduler to correctly offload these tensors and reuse the compute graph structure.
  • Result: Subsequent drafts in the loop are significantly faster (e.g., ~2.4ms vs 9.6ms) because the graph overhead is amortized.

Performance

Tested on a Windows workstation (Threadripper 5965WX + 2x RTX 3090).
Settings: Small prompt (8 tokens), average of 5 interactions.

Old Branch:

  • Baseline (No MTP): ~12.19 t/s
  • MTP (1 draft): ~10.52 t/s (Regression)
  • MTP (2 drafts): ~10.63 t/s

Updated Branch (This PR):

  • Baseline (No MTP): ~12.89 t/s
  • MTP (1 draft): ~13.14 t/s (Gain)
  • MTP (2 drafts): ~13.54 t/s (Gain)
  • MTP (3 drafts): ~12.56 t/s (Diminishing returns due to rejection)

Update (12/13):

After further testing following recent reports, I re-evaluated the performance. I was unable to replicate the regression reported, but my results differ from the initial run.

It is worth noting that this branch (even without MTP enabled) performs similar than master in my tests.

GLM-4.5 Air:

  • Master: ~17.60 t/s
  • Branch (No MTP): ~18.10 t/s
  • MTP (1 Draft): ~15.64 t/s
  • MTP (2 Drafts): ~15.63 t/s
  • MTP (3 Drafts): ~15.97 t/s

GLM-4.6:

  • Master: ~5.18 t/s
  • Branch (No MTP): ~5.47 t/s
  • MTP (1 Draft): ~5.91 t/s
  • MTP (2 Drafts): ~5.78 t/s
  • MTP (3 Drafts): ~5.41 t/s

Note: I updated the launch command to explicitly offload the MTP layer (layer 46 for Air, 92 for 4.6) to the GPU, which provided a small performance boost.

Commands Used:

.\build\bin\release\llama-server.exe ^
    --model "F:\llm_models\glm-4.5-air_Q4_general\GLM-4.5-Air-IQ4_XS-00001-of-00002.gguf" ^
    --alias GLM-4.5-Air ^
    --ctx-size 36864 ^
    -ctk q8_0 -ctv q8_0 ^
    -fa 1 --verbose ^
    --n-gpu-layers 99 ^
    -b 2048 -ub 1500 ^
    -ot "blk\.(3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18)\.ffn_.*=CUDA0" ^
    -ot "blk\.(20|21|22|23|24|25|26|27|28|29|30|31|32|33|46)\.ffn_.*=CUDA1" ^
    --override-tensor exps=CPU ^
    --no-mmap  ^
    --numa distribute ^
    -mtp --draft-max 3 --draft-p-min 0.85 ^
    --threads 24 --threads-batch 36 ^
    --host 127.0.0.1 --port 8080
.\build\bin\release\llama-server.exe ^
    --model "F:\llm_models\glm-4.6\GLM-4.6-UD-IQ1_S-00001-of-00002.gguf" ^
    --alias GLM-4.6 ^
    --ctx-size 36864 ^
    -ctk q8_0 -ctv q8_0 ^
    -fa 1 --verbose ^
    --n-gpu-layers 99 ^
    -b 2048 -ub 1500 ^
    -ot "blk\.(3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19)\.ffn_.*=CUDA0" ^
    -ot "blk\.(20|21|22|23|24|25|26|27|28|29|30|31|32|33|92)\.ffn_.*=CUDA1" ^
    --override-tensor exps=CPU ^
    --no-mmap  ^
    --numa distribute ^
    -mtp --draft-max 3 --draft-p-min 0.85 ^
    --threads 24 --threads-batch 36 ^
    --host 127.0.0.1 --port 8080

Conclusion

This update finally achieves net positive performance over the baseline. I am personally eager to try this on GLM-4.6 with better quantization. I invite others to test this configuration and report findings, as MTP logic is complex and may behave differently across hardware setups.

Also tagging @InfernalDread, as you previously mentioned interest in trying MTP once the performance improved. This update finally yields positive gains over the baseline, so I would love to hear your feedback if you have time to test it.

Transparency Note: I used an LLM to help organize and polish the description of this PR to ensure the architectural changes were explained clearly.

@Stealt91

Copy link
Copy Markdown

Awesome! Thank you for the effort to make this actually worth using! Can't wait for further improvements!

@InfernalDread

Copy link
Copy Markdown

Hello again! Thank you for the mention! I will definitely be trying out these new improvements, can't wait to see what's to come!

@timkhronos

timkhronos commented Dec 10, 2025 •

Copy link
Copy Markdown

Hello! I have been meaning to try out these new improvements, however I get a segmentation fault during the MTP pass.
image-18
Any idea what could be causing this? I am using a single gpu + cpu linux system, and a q4 quant of 4.6.

I also get the following warnings when building.
Screenshot from 2025-12-11 00-09-34

@SamuelOliveirads

Copy link
Copy Markdown
Author

Any idea what could be causing this? I am using a single gpu + cpu linux system, and a q4 quant of 4.6.

@timkhronos I was able to replicate the problem. It occurs because you are using GLM-4.6, which wasn't supported in this PR (most of the work was done on an older branch before 4.6 existed).

I have downloaded a GLM-4.6 quant to test and will look deeper to understand and fix the incompatibility. In theory, the architecture should be the same (or very similar) to the 4.5 variant, so I should be able to patch it.

By the way, if anyone wants to test MTP right now, please use the GLM-4.5 variant, at least the Air works okay, probably the full version also.

@SamuelOliveirads

Copy link
Copy Markdown
Author

GLM-4.6 has a small architectural difference compared to the 4.5 variants, specifically in the MTP layer (shared weights vs separate). I just pushed a fix for that and it's working on my end now. Anyone who wants to try MTP with GLM-4.6 should be able to do so without issues

@timkhronos

Copy link
Copy Markdown

First of all, thank you.

After testing, this pr seems to suffer large performance regressions, both in TG and PP, even when not using '-mtp' in the launch arguments. On the latest mainline build I get around 6.2 tok/s TG and 100~120 PP. On this Pr without -mtp, I get around 2.2 tok/s TG and 30 PP. With -mtp I get around 2.7 tok/s TG, if I offload the MTP tensor{92} to the GPU.

@SamuelOliveirads

Copy link
Copy Markdown
Author

After testing, this pr seems to suffer large performance regressions, both in TG and PP, even when not using '-mtp' in the launch arguments. On the latest mainline build I get around 6.2 tok/s TG and 100~120 PP. On this Pr without -mtp, I get around 2.2 tok/s TG and 30 PP. With -mtp I get around 2.7 tok/s TG, if I offload the MTP tensor{92} to the GPU.

That's curious. I ran a couple of tests using main (2fbe3b7) and this branch across different commits to see if I had introduced changes that degrade performance, but I couldn't replicate the problem.

In my tests using GLM-4.5-Air and GLM-4.6, both main and this branch (without MTP enabled) gave statistically identical performance. I have updated the PR description to show my latest results.

I did find some interesting behavior with the Air model: the speed varies quite a bit between runs, even on the same commit. I don't know exactly what causes this—perhaps it's related to how the graph is constructed each time the model loads or even because I'm testing to few examples—but I didn't have this issue with the 4.6 version, which was very consistent.

I also want to thank you for the idea of fully loading the MTP layer on the GPU. It gave me a small but measurable performance boost (jumping from around 14.40 t/s to between 15.6 and 16.0 t/s).

@F1LM1
F1LM1 merged this pull request into F1LM1:glm4-moe-mtp Dec 21, 2025
F1LM1 pushed a commit that referenced this pull request Dec 21, 2025
…gml-org#16038)

Initalizing RESERVED_NAME in is_reserved_name() is not thread
safe and leads to corrupted memory when used from multiple threads
as can be seen in the asan trace below. This fixes the initialization
to make it thread-safe.

    #0 0x000100abd018 in std::__1::pair<std::__1::__hash_iterator<std::__1::__hash_node<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, void*>*>, bool> std::__1::__hash_table<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, std::__1::hash<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>>, std::__1::equal_to<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>>, std::__1::allocator<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>>>::__emplace_unique_key_args<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>> const&>(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>> const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>> const&) __hash_table:1565
    #1 0x000100ab0320 in SchemaConverter::visit(nlohmann::json_abi_v3_12_0::basic_json<nlohmann::json_abi_v3_12_0::ordered_map, std::__1::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long long, unsigned long long, double, std::__1::allocator, nlohmann::json_abi_v3_12_0::adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>, void> const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>> const&) json-schema-to-grammar.cpp:802
    #2 0x000100aafc48 in std::__1::__function::__func<build_grammar(std::__1::function<void (common_grammar_builder const&)> const&, common_grammar_options const&)::$_2, std::__1::allocator<build_grammar(std::__1::function<void (common_grammar_builder const&)> const&, common_grammar_options const&)::$_2>, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>> (std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>> const&, nlohmann::json_abi_v3_12_0::basic_json<nlohmann::json_abi_v3_12_0::ordered_map, std::__1::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long long, unsigned long long, double, std::__1::allocator, nlohmann::json_abi_v3_12_0::adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>, void> const&)>::operator()(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>> const&, nlohmann::json_abi_v3_12_0::basic_json<nlohmann::json_abi_v3_12_0::ordered_map, std::__1::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long long, unsigned long long, double, std::__1::allocator, nlohmann::json_abi_v3_12_0::adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>, void> const&) function.h:319
    #3 0x000100a2c938 in std::__1::__function::__func<common_chat_params_init_llama_3_x(minja::chat_template const&, templates_params const&, bool)::$_0::operator()(common_grammar_builder const&) const::'lambda'(nlohmann::json_abi_v3_12_0::basic_json<nlohmann::json_abi_v3_12_0::ordered_map, std::__1::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long long, unsigned long long, double, std::__1::allocator, nlohmann::json_abi_v3_12_0::adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>, void> const&), std::__1::allocator<common_chat_params_init_llama_3_x(minja::chat_template const&, templates_params const&, bool)::$_0::operator()(common_grammar_builder const&) const::'lambda'(nlohmann::json_abi_v3_12_0::basic_json<nlohmann::json_abi_v3_12_0::ordered_map, std::__1::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long long, unsigned long long, double, std::__1::allocator, nlohmann::json_abi_v3_12_0::adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>, void> const&)>, void (nlohmann::json_abi_v3_12_0::basic_json<nlohmann::json_abi_v3_12_0::ordered_map, std::__1::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long long, unsigned long long, double, std::__1::allocator, nlohmann::json_abi_v3_12_0::adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>, void> const&)>::operator()(nlohmann::json_abi_v3_12_0::basic_json<nlohmann::json_abi_v3_12_0::ordered_map, std::__1::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long long, unsigned long long, double, std::__1::allocator, nlohmann::json_abi_v3_12_0::adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>, void> const&) function.h:319
    #4 0x000100a139f8 in foreach_function(nlohmann::json_abi_v3_12_0::basic_json<nlohmann::json_abi_v3_12_0::ordered_map, std::__1::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long long, unsigned long long, double, std::__1::allocator, nlohmann::json_abi_v3_12_0::adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>, void> const&, std::__1::function<void (nlohmann::json_abi_v3_12_0::basic_json<nlohmann::json_abi_v3_12_0::ordered_map, std::__1::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long long, unsigned long long, double, std::__1::allocator, nlohmann::json_abi_v3_12_0::adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>, void> const&)> const&) chat.cpp:762
    #5 0x000100a2a7f4 in std::__1::__function::__func<common_chat_params_init_llama_3_x(minja::chat_template const&, templates_params const&, bool)::$_0, std::__1::allocator<common_chat_params_init_llama_3_x(minja::chat_template const&, templates_params const&, bool)::$_0>, void (common_grammar_builder const&)>::operator()(common_grammar_builder const&) function.h:319
    #6 0x000100aa98f4 in build_grammar(std::__1::function<void (common_grammar_builder const&)> const&, common_grammar_options const&) json-schema-to-grammar.cpp:982
    #7 0x0001009c9314 in common_chat_params_init_llama_3_x(minja::chat_template const&, templates_params const&, bool) chat.cpp:1110
    #8 0x0001009b8afc in common_chat_templates_apply_jinja(common_chat_templates const*, common_chat_templates_inputs const&) chat.cpp:1992
    ggml-org#9 0x0001009b533c in common_chat_templates_apply(common_chat_templates const*, common_chat_templates_inputs const&) chat.cpp:2074
    ggml-org#10 0x000100810120 in llamacpp_apply_chat_template+0x724 (predict_oai-98384e17fb94e863:arm64+0x100090120)
    ...

==45482==Register values:
 x[0] = 0x00006020004147f8   x[1] = 0x00006080000013c8   x[2] = 0x0000000000000000   x[3] = 0x0000604006289738
 x[4] = 0x0000000000000002   x[5] = 0x0000000000000001   x[6] = 0x04034000004b4000   x[7] = 0x0000000000000001
 x[8] = 0xbebebebebebebebe   x[9] = 0x17d7d7d7d7d7d7d7  x[10] = 0x00000c04000828ff  x[11] = 0x0000000000000001
x[12] = 0x000000002018d383  x[13] = 0x0000000000000000  x[14] = 0xfa0000000000fafa  x[15] = 0x000010700001ffff
x[16] = 0x000000019dc012c0  x[17] = 0x00000001021284f8  x[18] = 0x0000000000000000  x[19] = 0x00000001700acdc0
x[20] = 0x0000000000000002  x[21] = 0x000000002018d384  x[22] = 0x16dd16fd2e731151  x[23] = 0x0000007000020000
x[24] = 0x0000000100c69c08  x[25] = 0x0000000100c69c20  x[26] = 0x00006080000013c7  x[27] = 0x0000000100c69c00
x[28] = 0x00000001700acd60     fp = 0x00000001700aceb0     lr = 0x0000000100abce30     sp = 0x00000001700acd60
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV __hash_table:1565 in std::__1::pair<std::__1::__hash_iterator<std::__1::__hash_node<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, void*>*>, bool> std::__1::__hash_table<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, std::__1::hash<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>>, std::__1::equal_to<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>>, std::__1::allocator<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>>>::__emplace_unique_key_args<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>> const&>(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>> const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>> const&)
Thread T5 created by T0 here:
    #0 0x0001020b99d4 in pthread_create+0x5c (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x359d4)
    #1 0x000100873910 in std::sys::pal::unix::thread::Thread::new::h77254fdd87a28e05+0x118 (predict_oai-98384e17fb94e863:arm64+0x1000f3910)
    #2 0x0001007c7a1c in test::run_test::haeb3c2bcd5ed6cf6+0x76c (predict_oai-98384e17fb94e863:arm64+0x100047a1c)
    #3 0x0001007aedb0 in test::console::run_tests_console::he9d142d704f3a986+0x149c (predict_oai-98384e17fb94e863:arm64+0x10002edb0)
    #4 0x0001007c5758 in test::test_main::hf86a5e20735245b9+0x118 (predict_oai-98384e17fb94e863:arm64+0x100045758)
    #5 0x0001007c5da0 in test::test_main_static::h61ee9c8fd30abca0+0x54 (predict_oai-98384e17fb94e863:arm64+0x100045da0)
    ...

==45482==ABORTING
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants