Instructions to use FoolDev/Janus-35B-HERETIC with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use FoolDev/Janus-35B-HERETIC with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf FoolDev/Janus-35B-HERETIC:Q4_K_M # Run inference directly in the terminal: llama cli -hf FoolDev/Janus-35B-HERETIC:Q4_K_M
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf FoolDev/Janus-35B-HERETIC:Q4_K_M # Run inference directly in the terminal: llama cli -hf FoolDev/Janus-35B-HERETIC:Q4_K_M
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf FoolDev/Janus-35B-HERETIC:Q4_K_M # Run inference directly in the terminal: ./llama-cli -hf FoolDev/Janus-35B-HERETIC:Q4_K_M
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf FoolDev/Janus-35B-HERETIC:Q4_K_M # Run inference directly in the terminal: ./build/bin/llama-cli -hf FoolDev/Janus-35B-HERETIC:Q4_K_M
Use Docker
docker model run hf.co/FoolDev/Janus-35B-HERETIC:Q4_K_M
- Llama new
- LM Studio
- Jan
- vLLM
How to use FoolDev/Janus-35B-HERETIC with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "FoolDev/Janus-35B-HERETIC" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "FoolDev/Janus-35B-HERETIC", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/FoolDev/Janus-35B-HERETIC:Q4_K_M
- Ollama
How to use FoolDev/Janus-35B-HERETIC with Ollama:
ollama run hf.co/FoolDev/Janus-35B-HERETIC:Q4_K_M
- Unsloth Desktop
- Pi
How to use FoolDev/Janus-35B-HERETIC with Pi:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf FoolDev/Janus-35B-HERETIC:Q4_K_M
Configure the model in Pi
# Install Pi: npm install -g @earendil-works/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "llama-cpp": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "FoolDev/Janus-35B-HERETIC:Q4_K_M" } ] } } }Run Pi
# Start Pi in your project directory: pi
- Docker Model Runner
How to use FoolDev/Janus-35B-HERETIC with Docker Model Runner:
docker model run hf.co/FoolDev/Janus-35B-HERETIC:Q4_K_M
- Lemonade
How to use FoolDev/Janus-35B-HERETIC with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull FoolDev/Janus-35B-HERETIC:Q4_K_M
Run and chat with the model
lemonade run user.Janus-35B-HERETIC-Q4_K_M
List all available models
lemonade list
- Hermes Agent
How to use FoolDev/Janus-35B-HERETIC with Hermes Agent:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf FoolDev/Janus-35B-HERETIC:Q4_K_M
Configure Hermes
# Install Hermes: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash hermes setup # Point Hermes at the local server: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:8080/v1 hermes config set model.default FoolDev/Janus-35B-HERETIC:Q4_K_M
Run Hermes
hermes
- Atomic Chat
- OpenClaw
How to use FoolDev/Janus-35B-HERETIC with OpenClaw:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf FoolDev/Janus-35B-HERETIC:Q4_K_M
Configure OpenClaw
# Install OpenClaw: npm install -g openclaw@latest # Register the local server and set it as the default model: openclaw onboard --non-interactive --mode local \ --auth-choice custom-api-key \ --custom-base-url http://127.0.0.1:8080/v1 \ --custom-model-id "FoolDev/Janus-35B-HERETIC:Q4_K_M" \ --custom-provider-id llama-cpp \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
feat(build): stamp chat_template.jinja into the GGUF at build time
Browse files--chat-template-file is a llama.cpp flag. LM Studio, Jan, KoboldCpp, Open WebUI
and llama-cpp-python read the template embedded in the GGUF and have nowhere to
put it, so they were still getting the upstream XML tool-call form and the empty
completions it causes.
strip_mtp.py gains --chat-template FILE, which swaps tokenizer.chat_template in
the same pass that drops the MTP block; build.sh passes chat_template.jinja
automatically when present. A GGUF built here now needs no flags on any loader.
With --chat-template on an already-clean input the file is rewritten purely to
carry the new template, since the hardlink passthrough cannot. Tensor data is
copied byte-for-byte in every path. An empty template file is refused.
Verified on synthetic GGUFs across all six paths, including that a clean input
with no stamp still hardlinks (same inode) and that the real chat_template.jinja
reads back byte-identical after stamping.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ans2THL2VJutsXi6RSFe4Y
- CHANGELOG.md +24 -0
- README.md +17 -1
- scripts/build.sh +12 -1
- scripts/strip_mtp.py +57 -10
|
@@ -8,6 +8,30 @@ track the **tooling and documentation**, not the underlying base model.
|
|
| 8 |
|
| 9 |
## [Unreleased]
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
### Fixed (provenance, and a CLI-detection footgun)
|
| 12 |
- **`base_model_relation: finetune` → `quantized`.** Nothing is finetuned in this
|
| 13 |
repo; it repackages and quantizes an upstream abliteration. A new
|
|
|
|
| 8 |
|
| 9 |
## [Unreleased]
|
| 10 |
|
| 11 |
+
### Added (chat template is stamped into the GGUF at build time)
|
| 12 |
+
- **`scripts/strip_mtp.py` gained `--chat-template FILE`**, which replaces the
|
| 13 |
+
GGUF's `tokenizer.chat_template` in the same pass that drops the MTP block.
|
| 14 |
+
`scripts/build.sh` now passes `chat_template.jinja` automatically when the file
|
| 15 |
+
is present. This matters because `--chat-template-file` is a llama.cpp flag:
|
| 16 |
+
LM Studio, Jan, KoboldCpp, Open WebUI and llama-cpp-python all read the template
|
| 17 |
+
**embedded in the GGUF** and have nowhere to put that flag, so they were still
|
| 18 |
+
getting the upstream XML tool-call form and the empty completions that come with
|
| 19 |
+
it. A GGUF built here now carries the working template, and those loaders need
|
| 20 |
+
no flags at all.
|
| 21 |
+
- The script stays a strip tool: with `--chat-template` on an already-MTP-clean
|
| 22 |
+
input it rewrites the file purely to swap the template (the usual hardlink
|
| 23 |
+
passthrough cannot carry one), and still hardlinks when neither a strip nor a
|
| 24 |
+
stamp is needed. Tensor data is copied byte-for-byte in every path — no
|
| 25 |
+
re-quantization, no quality change. An empty template file is refused rather
|
| 26 |
+
than stamped.
|
| 27 |
+
- Verified against synthetic GGUFs across all six paths: MTP-strip alone;
|
| 28 |
+
MTP-strip plus stamp; clean input plus stamp (rewrites, block_count preserved,
|
| 29 |
+
tensor bytes intact); clean input with no stamp (hardlink, same inode); empty
|
| 30 |
+
template file rejected; and this repo's real `chat_template.jinja` stamped and
|
| 31 |
+
read back byte-identical.
|
| 32 |
+
- Documented in the README: a locally built GGUF needs no flag, and there is a
|
| 33 |
+
one-liner to restamp a GGUF you already have.
|
| 34 |
+
|
| 35 |
### Fixed (provenance, and a CLI-detection footgun)
|
| 36 |
- **`base_model_relation: finetune` → `quantized`.** Nothing is finetuned in this
|
| 37 |
repo; it repackages and quantizes an upstream abliteration. A new
|
|
@@ -126,7 +126,7 @@ baked in, so context past ~262K degrades). See
|
|
| 126 |
| `scripts/build.sh` | Pulls a GGUF from `llmfan46/Qwen3.8-27B-Ultra-Uncensored-Heretic-Native-MTP-Preserved-GGUF` (default Q4_K_M), runs it through `strip_mtp.py`, then runs `ollama create janus`. Quants published there: BF16, Q3_K_L, Q3_K_M, Q4_K_M, Q4_K_S, Q5_K_M, Q5_K_S, Q6_K, Q8_0. The bundled Q4_K_M is already this Heretic quant (stripped); use this to build the others locally. |
|
| 127 |
| `scripts/check_bridge_sync.py` | Run before pushing a `Modelfile` / `template` / `system` / `params` edit to verify the four configurations remain in sync. Exits 0 if in sync, 1 with a per-key diff if not. |
|
| 128 |
| `scripts/verify_arch.py` | Cross-checks the README Architecture bullets (layer count, hidden size, dense FFN width, native context, vocab) plus the underlying forward-pass structure (Gated Attention + Gated DeltaNet head dims, partial RoPE, full-attention interval) against the bundled GGUF's `qwen35` metadata. Run on demand (`python3 scripts/verify_arch.py`); loads the ~17 GB GGUF (LFS smudge required) and exits non-zero on any mismatch. `block_count` is checked against **64** — the MTP-stripped value — so an unstripped bundle reports 65 and fails. |
|
| 129 |
-
| `scripts/strip_mtp.py` | Drops the MTP / NextN layer from a `qwen35` GGUF (`python3 scripts/strip_mtp.py IN.gguf OUT.gguf`): removes the extra `blk.<last>.*` block, decrements `block_count`, drops `nextn_predict_layers`. Kept tensors are copied byte-for-byte (no re-quant); a conditional no-op (hardlink) on already-clean quants. **Required, not optional:** the base is MTP-preserved, so quants from the source repo carry a NextN block at index 64 that stock llama.cpp / Ollama cannot load. `build.sh` runs this on every fetched quant, and the bundled Q4_K_M ships already stripped (`block_count` 64). Parity with the dense sibling `FoolDev/Thanatos-27B-HERETIC`. |
|
| 130 |
| `scripts/smoke_test.sh` | Integration smoke test against a running Ollama daemon: server reachable, model loaded, `tools` capability present, chat round-trip, and no control-token leakage. `TOOLS_TEST=1` adds a tool-call round-trip. Defaults to `MODEL=janus`. |
|
| 131 |
| `scripts/bench.sh` | Measures tok/s from Ollama's `eval_count` / `eval_duration` over a short/medium/long prompt mix (with a discarded warmup). Defaults to `MODEL=janus`. |
|
| 132 |
| `scripts/load_bundle.sh` | Loads the bundled `Janus-27B.Q4_K_M.gguf` into Ollama as a local `janus` tag without an upstream pull (smudges the LFS pointer via `hf download` if needed, checks the arch is `qwen35`). |
|
|
@@ -201,6 +201,7 @@ Drop the GGUF into your loader of choice. The chat template is embedded in the G
|
|
| 201 |
llama-server -m Janus-27B.Q4_K_M.gguf --jinja --chat-template-file chat_template.jinja
|
| 202 |
```
|
| 203 |
|
|
|
|
| 204 |
See [Tool / function calling](#tool--function-calling) for what differs and why.
|
| 205 |
|
| 206 |
### Ollama
|
|
@@ -504,6 +505,21 @@ llama-server -m Janus-27B.Q4_K_M.gguf --jinja --chat-template-file chat_template
|
|
| 504 |
|
| 505 |
With that flag the wire format is identical on Ollama and llama.cpp, so the same client code works against either.
|
| 506 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
#### Example (Ollama, OpenAI-compatible API)
|
| 508 |
|
| 509 |
```python
|
|
|
|
| 126 |
| `scripts/build.sh` | Pulls a GGUF from `llmfan46/Qwen3.8-27B-Ultra-Uncensored-Heretic-Native-MTP-Preserved-GGUF` (default Q4_K_M), runs it through `strip_mtp.py`, then runs `ollama create janus`. Quants published there: BF16, Q3_K_L, Q3_K_M, Q4_K_M, Q4_K_S, Q5_K_M, Q5_K_S, Q6_K, Q8_0. The bundled Q4_K_M is already this Heretic quant (stripped); use this to build the others locally. |
|
| 127 |
| `scripts/check_bridge_sync.py` | Run before pushing a `Modelfile` / `template` / `system` / `params` edit to verify the four configurations remain in sync. Exits 0 if in sync, 1 with a per-key diff if not. |
|
| 128 |
| `scripts/verify_arch.py` | Cross-checks the README Architecture bullets (layer count, hidden size, dense FFN width, native context, vocab) plus the underlying forward-pass structure (Gated Attention + Gated DeltaNet head dims, partial RoPE, full-attention interval) against the bundled GGUF's `qwen35` metadata. Run on demand (`python3 scripts/verify_arch.py`); loads the ~17 GB GGUF (LFS smudge required) and exits non-zero on any mismatch. `block_count` is checked against **64** — the MTP-stripped value — so an unstripped bundle reports 65 and fails. |
|
| 129 |
+
| `scripts/strip_mtp.py` | Drops the MTP / NextN layer from a `qwen35` GGUF (`python3 scripts/strip_mtp.py IN.gguf OUT.gguf`): removes the extra `blk.<last>.*` block, decrements `block_count`, drops `nextn_predict_layers`. Kept tensors are copied byte-for-byte (no re-quant); a conditional no-op (hardlink) on already-clean quants. **Required, not optional:** the base is MTP-preserved, so quants from the source repo carry a NextN block at index 64 that stock llama.cpp / Ollama cannot load. With `--chat-template FILE` it also restamps the GGUF's embedded chat template in the same pass (see [Tool / function calling](#tool--function-calling)). `build.sh` runs this on every fetched quant, and the bundled Q4_K_M ships already stripped (`block_count` 64). Parity with the dense sibling `FoolDev/Thanatos-27B-HERETIC`. |
|
| 130 |
| `scripts/smoke_test.sh` | Integration smoke test against a running Ollama daemon: server reachable, model loaded, `tools` capability present, chat round-trip, and no control-token leakage. `TOOLS_TEST=1` adds a tool-call round-trip. Defaults to `MODEL=janus`. |
|
| 131 |
| `scripts/bench.sh` | Measures tok/s from Ollama's `eval_count` / `eval_duration` over a short/medium/long prompt mix (with a discarded warmup). Defaults to `MODEL=janus`. |
|
| 132 |
| `scripts/load_bundle.sh` | Loads the bundled `Janus-27B.Q4_K_M.gguf` into Ollama as a local `janus` tag without an upstream pull (smudges the LFS pointer via `hf download` if needed, checks the arch is `qwen35`). |
|
|
|
|
| 201 |
llama-server -m Janus-27B.Q4_K_M.gguf --jinja --chat-template-file chat_template.jinja
|
| 202 |
```
|
| 203 |
|
| 204 |
+
A GGUF built by `./scripts/build.sh` has that template baked in and needs no flag.
|
| 205 |
See [Tool / function calling](#tool--function-calling) for what differs and why.
|
| 206 |
|
| 207 |
### Ollama
|
|
|
|
| 505 |
|
| 506 |
With that flag the wire format is identical on Ollama and llama.cpp, so the same client code works against either.
|
| 507 |
|
| 508 |
+
> **A locally built GGUF needs no flag.** `./scripts/build.sh` (`make build`) now
|
| 509 |
+
> stamps `chat_template.jinja` into the GGUF it produces, in the same pass that
|
| 510 |
+
> strips the MTP block — so LM Studio, Jan, KoboldCpp and anything else that reads
|
| 511 |
+
> the *embedded* template get working tool calls with no flags at all. The
|
| 512 |
+
> `--chat-template-file` flag above is only needed for a GGUF that was not built
|
| 513 |
+
> that way: the bundled blob, or a quant pulled straight from upstream. To restamp
|
| 514 |
+
> one you already have:
|
| 515 |
+
>
|
| 516 |
+
> ```bash
|
| 517 |
+
> python3 scripts/strip_mtp.py IN.gguf OUT.gguf --chat-template chat_template.jinja
|
| 518 |
+
> ```
|
| 519 |
+
>
|
| 520 |
+
> On an already-MTP-clean input that rewrites the file purely to swap the template;
|
| 521 |
+
> tensor data is copied byte-for-byte either way, so there is no re-quantization.
|
| 522 |
+
|
| 523 |
#### Example (Ollama, OpenAI-compatible API)
|
| 524 |
|
| 525 |
```python
|
|
@@ -105,7 +105,18 @@ fi
|
|
| 105 |
# upstream safetensors under vLLM / SGLang, or the unstripped GGUF straight from
|
| 106 |
# the source repo — not this path.
|
| 107 |
STRIPPED_PATH="${GGUF_PATH%.gguf}.stripped.gguf"
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
GGUF_PATH="${STRIPPED_PATH}"
|
| 110 |
echo "[*] gguf (post-strip): ${GGUF_PATH}"
|
| 111 |
|
|
|
|
| 105 |
# upstream safetensors under vLLM / SGLang, or the unstripped GGUF straight from
|
| 106 |
# the source repo — not this path.
|
| 107 |
STRIPPED_PATH="${GGUF_PATH%.gguf}.stripped.gguf"
|
| 108 |
+
# Stamp this repo's chat template into the GGUF in the same pass. llama.cpp,
|
| 109 |
+
# LM Studio, Jan and KoboldCpp all read the template embedded in the GGUF, and
|
| 110 |
+
# the upstream one instructs the model to emit tool calls in an XML dialect that
|
| 111 |
+
# llama.cpp's --jinja grammar rejects — which surfaces as an empty completion the
|
| 112 |
+
# moment a `tools` array is present. Stamping it here means those loaders get
|
| 113 |
+
# working tool calls with no extra flags. Skipped if the file is absent.
|
| 114 |
+
STAMP_ARGS=()
|
| 115 |
+
if [[ -f "${ROOT}/chat_template.jinja" ]]; then
|
| 116 |
+
STAMP_ARGS=(--chat-template "${ROOT}/chat_template.jinja")
|
| 117 |
+
echo "[*] stamping chat_template.jinja into the built GGUF"
|
| 118 |
+
fi
|
| 119 |
+
python3 "${ROOT}/scripts/strip_mtp.py" "${GGUF_PATH}" "${STRIPPED_PATH}" ${STAMP_ARGS[@]+"${STAMP_ARGS[@]}"}
|
| 120 |
GGUF_PATH="${STRIPPED_PATH}"
|
| 121 |
echo "[*] gguf (post-strip): ${GGUF_PATH}"
|
| 122 |
|
|
@@ -21,11 +21,27 @@ or taking the unstripped GGUF straight from the source repo.
|
|
| 21 |
Every KEPT tensor is copied byte-for-byte (raw quantized data) — no
|
| 22 |
re-quantization, so no quality change to the model itself.
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
Usage:
|
| 25 |
python3 scripts/strip_mtp.py IN.gguf OUT.gguf
|
|
|
|
| 26 |
"""
|
| 27 |
from __future__ import annotations
|
| 28 |
|
|
|
|
| 29 |
import sys
|
| 30 |
|
| 31 |
import numpy as np
|
|
@@ -47,10 +63,27 @@ def decode_field(field):
|
|
| 47 |
|
| 48 |
|
| 49 |
def main() -> int:
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
reader = GGUFReader(src, "r")
|
| 56 |
arch = decode_field(reader.get_field("general.architecture"))
|
|
@@ -68,7 +101,10 @@ def main() -> int:
|
|
| 68 |
# caller always gets a valid output without touching real transformer layers.
|
| 69 |
has_nextn = decode_field(reader.get_field(f"{arch}.nextn_predict_layers")) or 0
|
| 70 |
last_is_mtp = any(t.name.startswith(prefix) and "nextn" in t.name.lower() for t in reader.tensors)
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
| 72 |
print(f"[=] no MTP/NextN layer (block_count={block_count}); copying through unchanged")
|
| 73 |
if src != dst:
|
| 74 |
import os
|
|
@@ -84,12 +120,15 @@ def main() -> int:
|
|
| 84 |
# We only ever drop the last block, so it MUST be the MTP/NextN block. If
|
| 85 |
# nextn_predict_layers is set but blk.<last>.* holds no NextN tensor, the MTP
|
| 86 |
# block isn't where we assume — refuse rather than drop a real transformer layer.
|
| 87 |
-
if not last_is_mtp:
|
| 88 |
print(f"[!] {arch}.nextn_predict_layers is set but blk.{last}.* has no NextN tensor; "
|
| 89 |
"refusing to strip (would drop a real transformer layer).", file=sys.stderr)
|
| 90 |
return 1
|
| 91 |
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
# Writer manages general.architecture itself; block_count/nextn we override/drop.
|
| 95 |
skip_keys = {
|
|
@@ -97,9 +136,11 @@ def main() -> int:
|
|
| 97 |
f"{arch}.block_count",
|
| 98 |
f"{arch}.nextn_predict_layers",
|
| 99 |
}
|
|
|
|
|
|
|
| 100 |
|
| 101 |
writer = GGUFWriter(dst, arch=arch)
|
| 102 |
-
writer.add_block_count(block_count - 1)
|
| 103 |
|
| 104 |
n_kv = 0
|
| 105 |
for name, field in reader.fields.items():
|
|
@@ -117,8 +158,14 @@ def main() -> int:
|
|
| 117 |
writer.add_key_value(name, val, vtype)
|
| 118 |
n_kv += 1
|
| 119 |
|
| 120 |
-
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
print(f"[*] copied {n_kv} KV keys; keeping {len(kept)} tensors, dropping {len(dropped)} (block {last})")
|
| 123 |
|
| 124 |
for t in kept:
|
|
|
|
| 21 |
Every KEPT tensor is copied byte-for-byte (raw quantized data) — no
|
| 22 |
re-quantization, so no quality change to the model itself.
|
| 23 |
|
| 24 |
+
Optionally also restamp the embedded chat template in the same pass:
|
| 25 |
+
|
| 26 |
+
--chat-template FILE replace tokenizer.chat_template with FILE's contents
|
| 27 |
+
|
| 28 |
+
That matters because llama.cpp, LM Studio, Jan and KoboldCpp all read the
|
| 29 |
+
template baked into the GGUF, and the upstream one emits tool calls in an XML
|
| 30 |
+
dialect that llama.cpp's --jinja grammar rejects — the caller just gets an empty
|
| 31 |
+
completion. Stamping this repo's chat_template.jinja in at build time fixes tool
|
| 32 |
+
calling for every one of those loaders with no extra flags. build.sh does this
|
| 33 |
+
automatically when chat_template.jinja is present.
|
| 34 |
+
|
| 35 |
+
With --chat-template and no MTP block to strip, the file is still rewritten (the
|
| 36 |
+
usual hardlink passthrough would not carry the new template).
|
| 37 |
+
|
| 38 |
Usage:
|
| 39 |
python3 scripts/strip_mtp.py IN.gguf OUT.gguf
|
| 40 |
+
python3 scripts/strip_mtp.py IN.gguf OUT.gguf --chat-template chat_template.jinja
|
| 41 |
"""
|
| 42 |
from __future__ import annotations
|
| 43 |
|
| 44 |
+
import argparse
|
| 45 |
import sys
|
| 46 |
|
| 47 |
import numpy as np
|
|
|
|
| 63 |
|
| 64 |
|
| 65 |
def main() -> int:
|
| 66 |
+
ap = argparse.ArgumentParser(
|
| 67 |
+
description="Strip the MTP/NextN block from a GGUF; optionally restamp its chat template.")
|
| 68 |
+
ap.add_argument("src", metavar="IN.gguf")
|
| 69 |
+
ap.add_argument("dst", metavar="OUT.gguf")
|
| 70 |
+
ap.add_argument("--chat-template", metavar="FILE", default=None,
|
| 71 |
+
help="replace tokenizer.chat_template with the contents of FILE")
|
| 72 |
+
args = ap.parse_args()
|
| 73 |
+
src, dst = args.src, args.dst
|
| 74 |
+
|
| 75 |
+
new_template = None
|
| 76 |
+
if args.chat_template:
|
| 77 |
+
try:
|
| 78 |
+
with open(args.chat_template, encoding="utf-8") as fh:
|
| 79 |
+
new_template = fh.read()
|
| 80 |
+
except OSError as exc:
|
| 81 |
+
print(f"[!] cannot read {args.chat_template}: {exc}", file=sys.stderr)
|
| 82 |
+
return 1
|
| 83 |
+
if not new_template.strip():
|
| 84 |
+
print(f"[!] {args.chat_template} is empty; refusing to stamp a blank template",
|
| 85 |
+
file=sys.stderr)
|
| 86 |
+
return 1
|
| 87 |
|
| 88 |
reader = GGUFReader(src, "r")
|
| 89 |
arch = decode_field(reader.get_field("general.architecture"))
|
|
|
|
| 101 |
# caller always gets a valid output without touching real transformer layers.
|
| 102 |
has_nextn = decode_field(reader.get_field(f"{arch}.nextn_predict_layers")) or 0
|
| 103 |
last_is_mtp = any(t.name.startswith(prefix) and "nextn" in t.name.lower() for t in reader.tensors)
|
| 104 |
+
strip = bool(has_nextn or last_is_mtp)
|
| 105 |
+
# The hardlink passthrough cannot carry a new chat template, so it only
|
| 106 |
+
# applies when there is nothing at all to rewrite.
|
| 107 |
+
if not strip and new_template is None:
|
| 108 |
print(f"[=] no MTP/NextN layer (block_count={block_count}); copying through unchanged")
|
| 109 |
if src != dst:
|
| 110 |
import os
|
|
|
|
| 120 |
# We only ever drop the last block, so it MUST be the MTP/NextN block. If
|
| 121 |
# nextn_predict_layers is set but blk.<last>.* holds no NextN tensor, the MTP
|
| 122 |
# block isn't where we assume — refuse rather than drop a real transformer layer.
|
| 123 |
+
if strip and not last_is_mtp:
|
| 124 |
print(f"[!] {arch}.nextn_predict_layers is set but blk.{last}.* has no NextN tensor; "
|
| 125 |
"refusing to strip (would drop a real transformer layer).", file=sys.stderr)
|
| 126 |
return 1
|
| 127 |
|
| 128 |
+
if strip:
|
| 129 |
+
print(f"[*] arch={arch} block_count={block_count} -> stripping MTP block {last}, new block_count={block_count-1}")
|
| 130 |
+
else:
|
| 131 |
+
print(f"[=] arch={arch} block_count={block_count}; no MTP block, rewriting to restamp the chat template")
|
| 132 |
|
| 133 |
# Writer manages general.architecture itself; block_count/nextn we override/drop.
|
| 134 |
skip_keys = {
|
|
|
|
| 136 |
f"{arch}.block_count",
|
| 137 |
f"{arch}.nextn_predict_layers",
|
| 138 |
}
|
| 139 |
+
if new_template is not None:
|
| 140 |
+
skip_keys.add("tokenizer.chat_template")
|
| 141 |
|
| 142 |
writer = GGUFWriter(dst, arch=arch)
|
| 143 |
+
writer.add_block_count(block_count - 1 if strip else block_count)
|
| 144 |
|
| 145 |
n_kv = 0
|
| 146 |
for name, field in reader.fields.items():
|
|
|
|
| 158 |
writer.add_key_value(name, val, vtype)
|
| 159 |
n_kv += 1
|
| 160 |
|
| 161 |
+
if new_template is not None:
|
| 162 |
+
writer.add_string("tokenizer.chat_template", new_template)
|
| 163 |
+
n_kv += 1
|
| 164 |
+
print(f"[*] chat template restamped from {args.chat_template} "
|
| 165 |
+
f"({len(new_template)} chars)")
|
| 166 |
+
|
| 167 |
+
kept = [t for t in reader.tensors if not (strip and t.name.startswith(prefix))]
|
| 168 |
+
dropped = [t.name for t in reader.tensors if strip and t.name.startswith(prefix)]
|
| 169 |
print(f"[*] copied {n_kv} KV keys; keeping {len(kept)} tensors, dropping {len(dropped)} (block {last})")
|
| 170 |
|
| 171 |
for t in kept:
|