tarruda commited on
Commit
78005e9
·
0 Parent(s):

Super-squash branch 'main' using huggingface_hub

Browse files
.gitattributes ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.gguf filter=lfs diff=lfs merge=lfs -text
IQ4_XS/Step-3.7-Flash-IQ4_XS-00001-of-00004.gguf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6a231fe5ab197cbb320c879f3577eaa819a5576ed9485b3adcadc4ed7aed1897
3
+ size 5227136
IQ4_XS/Step-3.7-Flash-IQ4_XS-00002-of-00004.gguf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f1536e96c4d97b43f3041f6d0752bad480ab5b8fe4224cea303610da65b88a8e
3
+ size 49971170976
IQ4_XS/Step-3.7-Flash-IQ4_XS-00003-of-00004.gguf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0f62dbebc2568ca3f1bd22e22fb708412bb51244c9d3d23207f9aa70f654dbef
3
+ size 49333737984
IQ4_XS/Step-3.7-Flash-IQ4_XS-00004-of-00004.gguf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2e856252a87585693b769a752e744df636f61d8b9347362a0b4c8bc9c65505f8
3
+ size 7424323584
README.md ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ quantized_by: tarruda
3
+ pipeline_tag: text-generation
4
+ base_model: stepfun-ai/Step-3.7-Flash
5
+ base_model_relation: quantized
6
+ license: apache-2.0
7
+ license_link: https://www.apache.org/licenses/LICENSE-2.0.txt
8
+ tags:
9
+ - gguf
10
+ - imatrix
11
+ - conversational
12
+ - multimodal
13
+ - vision
14
+ - llama.cpp
15
+ ---
16
+
17
+ # Step 3.7 Flash GGUF
18
+
19
+ My custom IQ4_XS GGUF quantization for
20
+ [stepfun-ai/Step-3.7-Flash](https://huggingface.co/stepfun-ai/Step-3.7-Flash)
21
+
22
+ I've also modified the chat template also adds a `preserve_thinking` option,
23
+ which preserves thinking across user turns and can improve the experience when
24
+ prompt processing speed is a bottleneck.
25
+
26
+ ## Quant Recipes
27
+
28
+ | Recipe | Quant Size | Default type | Tensor-specific overrides |
29
+ | --- | --- | --- | --- |
30
+ | `IQ4_XS` | 101784.88 MiB (4.34 BPW) | `Q6_K` | `ffn_down_exps=iq4_xs`, `ffn_gate_exps=iq4_xs`, `ffn_up_exps=iq4_xs` |
31
+
32
+ ## Related Files
33
+
34
+ | File | Description |
35
+ | --- | --- |
36
+ | `Step-3.7-Flash-MTP-Q8_0.gguf` | Q8_0 MTP weights |
37
+ | `Step-3.7-Flash-mmproj-BF16.gguf` | BF16 multimodal projector |
38
+ | `Step-3.7-Flash-mmproj-F16.gguf` | F16 multimodal projector |
39
+ | `Step-3.7-Flash-mmproj-Q8_0.gguf` | Q8_0 multimodal projector |
40
+
41
+ ## Usage
42
+
43
+ Here's an example script:
44
+
45
+ ```bash
46
+ #!/bin/sh -e
47
+
48
+ model="./IQ4_XS/Step-3.7-Flash-IQ4_XS-00001-of-00004.gguf"
49
+ mmproj="./Step-3.7-Flash-mmproj-Q8_0.gguf"
50
+ mtp=./Step-3.7-Flash-MTP-Q8_0.gguf
51
+
52
+ ctx=262144
53
+ parallel=1
54
+
55
+ ctx_size=$((ctx * parallel))
56
+
57
+ reasoning_budget_message="...
58
+
59
+ Actually, I will stop now.
60
+
61
+ Let me provide the user with a comprehensive answer."
62
+
63
+ llama-server --no-mmap --no-warmup --model $model --mmproj $mmproj \
64
+ --ctx-size $ctx_size -np $parallel --temp 1.0 --top-p 0.95 \
65
+ --repeat-penalty 1.0 --presence-penalty 0.0 \
66
+ --reasoning-budget-message "$reasoning_budget_message" \
67
+ --reasoning-preserve \
68
+ --spec-type draft-mtp -md $mtp --spec-draft-n-max 3 --spec-draft-p-min 0.65 \
69
+ -ctxcp 8 --checkpoint-min-step 512 \
70
+ --cache-ram 4096
71
+ ```
Step-3.7-Flash-MTP-Q8_0.gguf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b0bf32474ec4b491e267b2344022d4a795dbe7647e64a6b349ecedbab46fce79
3
+ size 3707271936
Step-3.7-Flash-mmproj-BF16.gguf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:123026a9c9d9221af27b6d798fda36c04721fd9ad0dd81d9f3791a5100b9ce9f
3
+ size 4399308608
Step-3.7-Flash-mmproj-F16.gguf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a1695d4541d727a67604ea8f2070b383fff0767e49abe76b4a51632f3b2a52c9
3
+ size 3972828992
Step-3.7-Flash-mmproj-Q8_0.gguf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0fb4b625241216272b388fd13ab5afc7409d3728481d8efc5c2841c25ac006d3
3
+ size 2747064128
chat_template.jinja ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% macro render_message_content(message) %}{% if message.content is none %}{{- '' }}{% elif message.content is string %}{{- message.content }}{% elif message.content is mapping %}{{- message.content['value'] if 'value' in message.content else message.content['text'] }}{% elif message.content is iterable %}{% set ns = namespace(needs_text_separator=false) %}{% for item in message.content %}{% if item.type == 'text' %}{% if ns.needs_text_separator %}{{- ' ' }}{% endif %}{{- item['value'] if 'value' in item else item['text'] }}{% set ns.needs_text_separator = true %}{% elif item.type == 'image' %}<im_patch>{% set ns.needs_text_separator = false %}{% endif %}{% endfor %}{% endif %}{% endmacro %}
2
+ {{bos_token}}{%- if tools %}
3
+ {{- '<|im_start|>system\n' }}
4
+ {%- if reasoning_effort is defined %}
5
+ {{- "Reasoning: " + reasoning_effort + '\n\n' }}
6
+ {%- endif %}
7
+ {%- if messages[0].role == 'system' %}
8
+ {{- render_message_content(messages[0]) + '\n\n' }}
9
+ {%- endif %}
10
+ {{- "# Tools\n\nYou have access to the following functions in JSONSchema format:\n\n<tools>" }}
11
+ {%- for tool in tools %}
12
+ {{- "\n" }}
13
+ {{- tool | tojson(ensure_ascii=False) }}
14
+ {%- endfor %}
15
+ {{- "\n</tools>\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...>\n...\n</function> block must be nested within <tool_call>\n...\n</tool_call> XML tags\n- Required parameters MUST be specified\n</IMPORTANT><|im_end|>\n" }}
16
+ {%- else %}
17
+ {%- if messages[0].role == 'system' %}
18
+ {{- '<|im_start|>system\n' }}
19
+ {%- if reasoning_effort is defined %}
20
+ {{- "Reasoning: " + reasoning_effort + '\n\n' }}
21
+ {%- endif %}
22
+ {{- render_message_content(messages[0]) + '<|im_end|>\n' }}
23
+ {%- elif reasoning_effort is defined %}
24
+ {{- '<|im_start|>system\n' + "Reasoning: " + reasoning_effort + '\n\n' + '<|im_end|>\n' }}
25
+ {%- endif %}
26
+ {%- endif %}
27
+ {%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
28
+ {%- for message in messages[::-1] %}
29
+ {%- set index = (messages|length - 1) - loop.index0 %}
30
+ {%- if ns.multi_step_tool and message.role == "user" and render_message_content(message) is string and not(render_message_content(message).startswith('<tool_response>') and render_message_content(message).endswith('</tool_response>')) %}
31
+ {%- set ns.multi_step_tool = false %}
32
+ {%- set ns.last_query_index = index %}
33
+ {%- endif %}
34
+ {%- endfor %}
35
+ {%- for message in messages %}
36
+ {%- set content = render_message_content(message) %}
37
+ {%- if (message.role == "user") or (message.role == "system" and not loop.first) %}
38
+ {%- set role_name = 'observation' if (message.role == "system" and not loop.first and message.name == 'observation') else message.role %}
39
+ {{- '<|im_start|>' + role_name + '\n' + content + '<|im_end|>' + '\n' }}
40
+ {%- elif message.role == "assistant" %}
41
+ {%- if message.reasoning_content is string %}
42
+ {%- set reasoning_content = message.reasoning_content %}
43
+ {%- else %}
44
+ {%- if '</think>' in content %}
45
+ {%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
46
+ {%- set content = content.split('</think>')[-1].lstrip('\n') %}
47
+ {%- else %}
48
+ {%- set reasoning_content = '' %}
49
+ {%- endif %}
50
+ {%- endif %}
51
+ {%- set reasoning_content = reasoning_content|trim %}
52
+ {%- set content = content|trim %}
53
+ {%- if (preserve_thinking is defined and preserve_thinking is true) or (loop.index0 > ns.last_query_index) %}
54
+ {{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content + '\n</think>\n' + content }}
55
+ {%- else %}
56
+ {{- '<|im_start|>' + message.role + '\n' + content }}
57
+ {%- endif %}
58
+ {%- if message.tool_calls %}
59
+ {%- for tool_call in message.tool_calls %}
60
+ {%- if tool_call.function is defined %}
61
+ {%- set tool_call = tool_call.function %}
62
+ {%- endif %}
63
+ {{- '<tool_call>\n<function=' + tool_call.name + '>\n' }}
64
+ {%- if tool_call.arguments is defined %}
65
+ {%- set arguments = tool_call.arguments | fromjson if tool_call.arguments is string else tool_call.arguments %}
66
+ {%- for args_name, args_value in arguments|items %}
67
+ {{- '<parameter=' + args_name + '>\n' }}
68
+ {%- set args_value = args_value | tojson(ensure_ascii=False) | safe if args_value is mapping or (args_value is sequence and args_value is not string) else args_value | string %}
69
+ {{- args_value }}
70
+ {{- '\n</parameter>\n' }}
71
+ {%- endfor %}
72
+ {%- endif %}
73
+ {{- '</function>\n</tool_call>' }}
74
+ {%- endfor %}
75
+ {%- endif %}
76
+ {{- '<|im_end|>\n' }}
77
+ {%- elif message.role == "tool" %}
78
+ {%- if loop.first or (messages[loop.index0 - 1].role != "tool") %}
79
+ {{- '<|im_start|>tool_response\n' }}
80
+ {%- endif %}
81
+ {{- '<tool_response>' }}
82
+ {{- content }}
83
+ {{- '</tool_response>' }}
84
+ {%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
85
+ {{- '<|im_end|>\n' }}
86
+ {%- endif %}
87
+ {%- endif %}
88
+ {%- endfor %}
89
+ {%- if add_generation_prompt %}
90
+ {{- '<|im_start|>assistant\n<think>\n' }}
91
+ {%- endif %}
imatrix.gguf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7f94ca213e4560d30b492b332128527c6808041ec3526df6c2816884eb107203
3
+ size 465998112
scripts/convert-to-gguf.sh ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ # Validate that exactly 2 arguments are provided
6
+ if [ $# -ne 2 ]; then
7
+ echo "Error: Exactly 2 arguments required."
8
+ echo "Usage: $0 <llama_cpp_dir> <original_safetensors_path>"
9
+ echo "Example: $0 ~/code/llama.cpp ../../Qwen/Qwen3.5-397B-A10B"
10
+ exit 1
11
+ fi
12
+
13
+ # Assign arguments to variables for clarity
14
+ LLAMA_CPP_DIR="$1"
15
+ ORIGINAL_SAFETENSORS_PATH="$2"
16
+ CONVERSION_MODEL_PATH="$ORIGINAL_SAFETENSORS_PATH"
17
+
18
+ # Validate that the llama.cpp directory exists
19
+ if [ ! -d "$LLAMA_CPP_DIR" ]; then
20
+ echo "Error: llama.cpp directory not found: $LLAMA_CPP_DIR"
21
+ exit 1
22
+ fi
23
+
24
+ # Construct the path to the conversion script
25
+ CONVERT_TO_GGUF_PATH="$LLAMA_CPP_DIR/convert_hf_to_gguf.py"
26
+
27
+ # Validate that the Python script exists
28
+ if [ ! -f "$CONVERT_TO_GGUF_PATH" ]; then
29
+ echo "Error: Python script not found: $CONVERT_TO_GGUF_PATH"
30
+ exit 1
31
+ fi
32
+
33
+ # Validate that the model path exists
34
+ if [ ! -e "$ORIGINAL_SAFETENSORS_PATH" ]; then
35
+ echo "Error: Model path not found: $ORIGINAL_SAFETENSORS_PATH"
36
+ PARENT_DIR="$(dirname "$ORIGINAL_SAFETENSORS_PATH")"
37
+ BASE_NAME="$(basename "$ORIGINAL_SAFETENSORS_PATH")"
38
+ if [ -d "$PARENT_DIR" ]; then
39
+ CANDIDATE="$(find "$PARENT_DIR" -maxdepth 1 -type d -name "${BASE_NAME}*" -print -quit 2>/dev/null || true)"
40
+ if [ -n "$CANDIDATE" ]; then
41
+ echo "Did you mean: $CANDIDATE"
42
+ fi
43
+ fi
44
+ exit 1
45
+ fi
46
+
47
+ ORIGINAL_SAFETENSORS_PATH="$(cd "$(dirname "$ORIGINAL_SAFETENSORS_PATH")" && pwd)/$(basename "$ORIGINAL_SAFETENSORS_PATH")"
48
+ CONVERSION_MODEL_PATH="$ORIGINAL_SAFETENSORS_PATH"
49
+ MODEL_NAME="$(basename "$ORIGINAL_SAFETENSORS_PATH")"
50
+ # Get the directory where the script is located
51
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
52
+ CONVERTER_SHIM="$SCRIPT_DIR/convert_hf_to_gguf_step37.py"
53
+ if [ ! -f "$CONVERTER_SHIM" ]; then
54
+ echo "Error: converter shim not found: $CONVERTER_SHIM"
55
+ exit 1
56
+ fi
57
+
58
+ # Resolve GGUF_OUT_DIR absolutely (parent of script directory)
59
+ # We cd into the parent and run pwd to get the absolute path
60
+ if [ ! -d "$SCRIPT_DIR/.." ]; then
61
+ echo "Error: Could not resolve parent directory for output."
62
+ exit 1
63
+ fi
64
+ GGUF_OUT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
65
+ OVERLAY_PARENT="$GGUF_OUT_DIR/.conversion-model"
66
+ OVERLAY_MODEL_PATH="$OVERLAY_PARENT/$MODEL_NAME"
67
+
68
+ # Define BF16_DIR inside GGUF_OUT_DIR
69
+ BF16_DIR="$GGUF_OUT_DIR/BF16"
70
+ mkdir -pv "$BF16_DIR"
71
+
72
+ # Activate virtual environment inside llama.cpp directory
73
+ if [ ! -f "$LLAMA_CPP_DIR/.venv/bin/activate" ]; then
74
+ echo "Error: Python virtual environment not found: $LLAMA_CPP_DIR/.venv"
75
+ exit 1
76
+ fi
77
+ cd "$LLAMA_CPP_DIR"
78
+ source .venv/bin/activate
79
+ PYTHON_BIN="$(command -v python)"
80
+ cd - >/dev/null
81
+
82
+ run_converter() {
83
+ "$PYTHON_BIN" "$CONVERTER_SHIM" "$CONVERT_TO_GGUF_PATH" "$@"
84
+ }
85
+
86
+ # Execute the Python script with the provided arguments
87
+ # Main conversion output goes to BF16_DIR
88
+
89
+ run_converter --no-mtp --no-tensor-first-split --split-max-size 50G --outfile "$BF16_DIR/${MODEL_NAME}-BF16.gguf" "$CONVERSION_MODEL_PATH"
90
+
91
+ run_converter --mtp --outfile "$GGUF_OUT_DIR/${MODEL_NAME}-MTP-Q8_0.gguf" --outtype q8_0 "$ORIGINAL_SAFETENSORS_PATH"
92
+
93
+ # # mmproj output goes to GGUF_OUT_DIR
94
+ run_converter --mmproj --outtype bf16 --outfile "$GGUF_OUT_DIR/${MODEL_NAME}-mmproj-BF16.gguf" "$CONVERSION_MODEL_PATH"
95
+ # # also do FP16 and Q8_0 versions
96
+ run_converter --mmproj --outtype f16 --outfile "$GGUF_OUT_DIR/${MODEL_NAME}-mmproj-F16.gguf" "$CONVERSION_MODEL_PATH"
97
+ run_converter --mmproj --outtype q8_0 --outfile "$GGUF_OUT_DIR/${MODEL_NAME}-mmproj-Q8_0.gguf" "$CONVERSION_MODEL_PATH"
98
+
99
+ rm -rf "$OVERLAY_PARENT"
scripts/convert_hf_to_gguf_step37.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import ast
5
+ import importlib.util
6
+ import inspect
7
+ import sys
8
+ import textwrap
9
+ from hashlib import sha256
10
+ from pathlib import Path
11
+ from types import ModuleType
12
+ from typing import Any, Callable
13
+
14
+
15
+ STEP37_TOKENIZER_HASH = "5841594bd6a8eeecd7207aeec6570831cc97ffaeba51e908bdaf560113177bae"
16
+ STEP37_TOKENIZER_PRE = "deepseek-v3"
17
+
18
+
19
+ def load_module(path: Path) -> ModuleType:
20
+ spec = importlib.util.spec_from_file_location("_llama_convert_hf_to_gguf", path)
21
+ if spec is None or spec.loader is None:
22
+ raise RuntimeError(f"Could not load converter module from {path}")
23
+
24
+ module = importlib.util.module_from_spec(spec)
25
+ sys.modules[spec.name] = module
26
+ spec.loader.exec_module(module)
27
+ return module
28
+
29
+
30
+ def checker_text(original: Callable[..., str]) -> str | None:
31
+ try:
32
+ source = textwrap.dedent(inspect.getsource(original))
33
+ tree = ast.parse(source)
34
+ except Exception:
35
+ return None
36
+
37
+ for node in ast.walk(tree):
38
+ if not isinstance(node, ast.Assign):
39
+ continue
40
+ if any(isinstance(target, ast.Name) and target.id == "chktxt" for target in node.targets):
41
+ try:
42
+ return ast.literal_eval(node.value)
43
+ except Exception:
44
+ return None
45
+
46
+ return None
47
+
48
+
49
+ def patch_step37_tokenizer() -> None:
50
+ from conversion import base
51
+
52
+ original = base.TextModel.get_vocab_base_pre
53
+ chktxt = checker_text(original)
54
+
55
+ def patched_get_vocab_base_pre(self: Any, tokenizer: Any) -> str:
56
+ if chktxt is not None:
57
+ try:
58
+ chkhsh = sha256(str(tokenizer.encode(chktxt)).encode()).hexdigest()
59
+ except Exception:
60
+ chkhsh = None
61
+
62
+ if chkhsh == STEP37_TOKENIZER_HASH:
63
+ return STEP37_TOKENIZER_PRE
64
+
65
+ return original(self, tokenizer)
66
+
67
+ base.TextModel.get_vocab_base_pre = patched_get_vocab_base_pre
68
+
69
+
70
+ def main() -> None:
71
+ if len(sys.argv) < 2:
72
+ raise SystemExit("Usage: convert_hf_to_gguf_step37.py <convert_hf_to_gguf.py> [converter args...]")
73
+
74
+ convert_path = Path(sys.argv[1]).expanduser().resolve()
75
+ if not convert_path.is_file():
76
+ raise SystemExit(f"Converter not found: {convert_path}")
77
+
78
+ sys.path.insert(0, str(convert_path.parent))
79
+ module = load_module(convert_path)
80
+ patch_step37_tokenizer()
81
+
82
+ sys.argv = [str(convert_path), *sys.argv[2:]]
83
+ module.main()
84
+
85
+
86
+ if __name__ == "__main__":
87
+ main()
scripts/generate-imatrix.sh ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ # Validate that exactly 2 arguments are provided
6
+ if [ $# -ne 2 ]; then
7
+ echo "Error: Exactly 2 arguments required."
8
+ echo "Usage: $0 <llama_cpp_dir> <gguf>"
9
+ echo "Example: $0 ~/code/llama.cpp BF16/Qwen3.5-35B-A3B-BF16-00001-of-00003.gguf"
10
+ exit 1
11
+ fi
12
+
13
+ # Assign arguments to variables for clarity
14
+ LLAMA_CPP_DIR="$1"
15
+ GGUF_PATH="$2"
16
+
17
+ # Validate that the llama.cpp directory exists
18
+ if [ ! -d "$LLAMA_CPP_DIR" ]; then
19
+ echo "Error: llama.cpp directory not found: $LLAMA_CPP_DIR"
20
+ exit 1
21
+ fi
22
+
23
+
24
+ # Validate that the Python script exists
25
+ if [ ! -e "$GGUF_PATH" ]; then
26
+ echo "Error: GGUF not found: $GGUF_PATH"
27
+ exit 1
28
+ fi
29
+
30
+ CALIBRATION_DATASET_PATH=$HOME/calibration-data.txt
31
+
32
+ if [ ! -e $CALIBRATION_DATASET_PATH ]; then
33
+ # download the calibration dataset
34
+ curl -L https://gist.githubusercontent.com/ubergarm/edfeb3ff9c6ec8b49e88cdf627b0711a/raw/ba5b01b6960a86874592f5913e283746ff734483/ubergarm-imatrix-calibration-corpus-v02.txt > $CALIBRATION_DATASET_PATH
35
+ fi
36
+
37
+ # Get the directory where the script is located
38
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
39
+ IMATRIX_OUT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
40
+ IMATRIX_PATH="$IMATRIX_OUT_DIR/imatrix.gguf"
41
+
42
+ if [ -e "$IMATRIX_PATH" ]; then
43
+ echo "Error: imatrix already exists: $IMATRIX_PATH"
44
+ exit 1
45
+ fi
46
+
47
+ $LLAMA_CPP_DIR/build/bin/llama-imatrix \
48
+ -fit off \
49
+ --model "$GGUF_PATH"\
50
+ -f $CALIBRATION_DATASET_PATH \
51
+ -o $IMATRIX_PATH \
52
+ --ctx-size 512 \
53
+ -ub 4096 -b 4096 \
54
+ --no-mmap
scripts/quantize.sh ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+ shopt -s extglob
5
+
6
+ common_tensor_types="
7
+ "
8
+
9
+ TYPE_DEFAULT_FALLBACK=Q4_K
10
+
11
+ recipes=(
12
+ "
13
+ RECIPE_NAME=IQ4_XS
14
+ TYPE_DEFAULT=q6_k
15
+
16
+ ffn_down_exps=iq4_xs
17
+ ffn_gate_exps=iq4_xs
18
+ ffn_up_exps=iq4_xs
19
+ "
20
+
21
+ )
22
+
23
+ recipe_line() {
24
+ local line="$1"
25
+ printf '%s' "${line##+([[:space:]])}"
26
+ }
27
+
28
+ recipe_value() {
29
+ local recipe="$1"
30
+ local key="$2"
31
+ local line
32
+
33
+ while IFS= read -r line || [ -n "$line" ]; do
34
+ line="$(recipe_line "$line")"
35
+ case "$line" in
36
+ "${key}="*)
37
+ printf '%s' "${line#*=}"
38
+ return 0
39
+ ;;
40
+ esac
41
+ done <<<"$recipe"
42
+
43
+ return 1
44
+ }
45
+
46
+ recipe_type_default() {
47
+ local recipe="$1"
48
+
49
+ recipe_value "$recipe" TYPE_DEFAULT || printf '%s' "$TYPE_DEFAULT_FALLBACK"
50
+ }
51
+
52
+ print_recipes() {
53
+ local recipe recipe_name type_default
54
+
55
+ for recipe in "${recipes[@]}"; do
56
+ recipe_name="$(recipe_value "$recipe" RECIPE_NAME)"
57
+ type_default="$(recipe_type_default "$recipe")"
58
+
59
+ printf ' %s:\n' "$recipe_name"
60
+ printf ' TYPE_DEFAULT=%s\n' "$type_default"
61
+ printf '\n'
62
+ done
63
+ }
64
+
65
+ SELECTED_RECIPE_NAME=
66
+ SELECTED_TYPE_DEFAULT=
67
+ SELECTED_RECIPE=
68
+
69
+ load_recipe() {
70
+ local requested_recipe="$1"
71
+ local recipe recipe_name
72
+
73
+ for recipe in "${recipes[@]}"; do
74
+ recipe_name="$(recipe_value "$recipe" RECIPE_NAME)"
75
+ if [ "$recipe_name" = "$requested_recipe" ]; then
76
+ SELECTED_RECIPE_NAME="$recipe_name"
77
+ SELECTED_TYPE_DEFAULT="$(recipe_type_default "$recipe")"
78
+ SELECTED_RECIPE="$recipe"
79
+ return 0
80
+ fi
81
+ done
82
+
83
+ return 1
84
+ }
85
+
86
+ append_tensor_type_lines() {
87
+ local block="$1"
88
+ local output_path="$2"
89
+ local skip_recipe_metadata="${3:-false}"
90
+ local line
91
+
92
+ while IFS= read -r line || [ -n "$line" ]; do
93
+ line="$(recipe_line "$line")"
94
+ if [ "$skip_recipe_metadata" = true ]; then
95
+ case "$line" in
96
+ RECIPE_NAME=* | TYPE_DEFAULT=*)
97
+ continue
98
+ ;;
99
+ esac
100
+ fi
101
+
102
+ if [ -z "$line" ]; then
103
+ continue
104
+ fi
105
+
106
+ printf '%s\n' "$line" >>"$output_path"
107
+ done <<<"$block"
108
+ }
109
+
110
+ write_tensor_type_file() {
111
+ local recipe="$1"
112
+ local output_path="$2"
113
+
114
+ : >"$output_path"
115
+ append_tensor_type_lines "$common_tensor_types" "$output_path"
116
+ append_tensor_type_lines "$recipe" "$output_path" true
117
+ }
118
+
119
+ usage() {
120
+ cat <<'EOF'
121
+ Usage: quantize.sh <llama_cpp_dir> <recipe_name> [--dry-run] [--split]
122
+
123
+ Outputs are written relative to the current directory.
124
+
125
+ Environment overrides:
126
+ INPUT_GGUF=<path> Input GGUF. Defaults to the first *BF16*.gguf in BF16/.
127
+ IMATRIX_PATH=<path> Importance matrix path. Defaults to imatrix.gguf.
128
+
129
+ Available recipes:
130
+ EOF
131
+ print_recipes
132
+ cat <<'EOF'
133
+ Examples:
134
+ ./scripts/quantize.sh ~/code/llama.cpp IQ3_XXS --dry-run
135
+ ./scripts/quantize.sh ~/code/llama.cpp IQ3_XXS
136
+ ./scripts/quantize.sh ~/code/llama.cpp IQ3_XXS --split
137
+ EOF
138
+ }
139
+
140
+ if [ $# -lt 2 ]; then
141
+ usage
142
+ exit 1
143
+ fi
144
+
145
+ LLAMA_CPP_DIR="$1"
146
+ REQUESTED_RECIPE="$2"
147
+ DRY_RUN=false
148
+ SPLIT=false
149
+ shift 2
150
+
151
+ while [ $# -gt 0 ]; do
152
+ case "$1" in
153
+ --dry-run)
154
+ DRY_RUN=true
155
+ ;;
156
+ --split)
157
+ SPLIT=true
158
+ ;;
159
+ *)
160
+ usage
161
+ exit 1
162
+ ;;
163
+ esac
164
+ shift
165
+ done
166
+
167
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
168
+ PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
169
+ BF16_DIR="$PROJECT_DIR/BF16"
170
+ SPLIT_SCRIPT="$SCRIPT_DIR/split.sh"
171
+
172
+ if ! load_recipe "$REQUESTED_RECIPE"; then
173
+ echo "Error: unknown recipe: $REQUESTED_RECIPE" >&2
174
+ usage >&2
175
+ exit 1
176
+ fi
177
+
178
+ if [ ! -d "$LLAMA_CPP_DIR" ]; then
179
+ echo "Error: llama.cpp directory not found: $LLAMA_CPP_DIR" >&2
180
+ exit 1
181
+ fi
182
+
183
+ QUANTIZE_BIN="$LLAMA_CPP_DIR/build/bin/llama-quantize"
184
+ SPLIT_BIN="$LLAMA_CPP_DIR/build/bin/llama-gguf-split"
185
+
186
+ if [ ! -x "$QUANTIZE_BIN" ]; then
187
+ echo "Error: llama-quantize binary not found: $QUANTIZE_BIN" >&2
188
+ exit 1
189
+ fi
190
+
191
+ if [ "$SPLIT" = true ] && [ ! -x "$SPLIT_SCRIPT" ]; then
192
+ echo "Error: split script not found or not executable: $SPLIT_SCRIPT" >&2
193
+ exit 1
194
+ fi
195
+
196
+ if [ "$SPLIT" = true ] && [ ! -x "$SPLIT_BIN" ]; then
197
+ echo "Error: llama-gguf-split binary not found: $SPLIT_BIN" >&2
198
+ exit 1
199
+ fi
200
+
201
+ if [ -z "${INPUT_GGUF-}" ]; then
202
+ if [ ! -d "$BF16_DIR" ]; then
203
+ echo "Error: BF16 directory not found: $BF16_DIR" >&2
204
+ exit 1
205
+ fi
206
+
207
+ INPUT_GGUF="$(find "$BF16_DIR" -maxdepth 1 -name "*BF16*.gguf" -type f | sort | head -n 1)"
208
+ fi
209
+
210
+ if [ -z "$INPUT_GGUF" ] || [ ! -e "$INPUT_GGUF" ]; then
211
+ echo "Error: input GGUF not found: ${INPUT_GGUF:-<none>}" >&2
212
+ exit 1
213
+ fi
214
+
215
+ IMATRIX_PATH="${IMATRIX_PATH:-$PROJECT_DIR/imatrix.gguf}"
216
+ if [ ! -e "$IMATRIX_PATH" ]; then
217
+ echo "Error: imatrix file not found: $IMATRIX_PATH" >&2
218
+ exit 1
219
+ fi
220
+
221
+ cd "$LLAMA_CPP_DIR"
222
+ if [ -f .venv/bin/activate ]; then
223
+ # shellcheck disable=SC1091
224
+ source .venv/bin/activate
225
+ fi
226
+ cd - >/dev/null
227
+
228
+ OUTPUT_BASE_DIR="$(pwd)"
229
+ INPUT_BASENAME="$(basename "$INPUT_GGUF")"
230
+ MODEL_NAME="$(printf '%s\n' "$INPUT_BASENAME" | sed -E 's/-BF16(-[0-9]+-of-[0-9]+)?\.gguf$//')"
231
+ INTERMEDIATE_OUTPUT="$OUTPUT_BASE_DIR/${MODEL_NAME}-${SELECTED_RECIPE_NAME}.gguf"
232
+ OUTPUT_DIR="$OUTPUT_BASE_DIR/$SELECTED_RECIPE_NAME"
233
+
234
+ if [ "$DRY_RUN" = false ] && [ -e "$INTERMEDIATE_OUTPUT" ]; then
235
+ echo "Error: quantized output already exists: $INTERMEDIATE_OUTPUT" >&2
236
+ exit 1
237
+ fi
238
+
239
+ TENSOR_TYPE_FILE="$(mktemp "${TMPDIR:-/tmp}/nex-n2-pro-tensor-types.XXXXXX")"
240
+ cleanup() {
241
+ rm -f "$TENSOR_TYPE_FILE"
242
+ }
243
+ trap cleanup EXIT
244
+
245
+ write_tensor_type_file "$SELECTED_RECIPE" "$TENSOR_TYPE_FILE"
246
+
247
+ echo "Input: $INPUT_GGUF"
248
+ echo "Recipe name: $SELECTED_RECIPE_NAME"
249
+ echo "Tensor type file: $TENSOR_TYPE_FILE"
250
+ echo "Default type: $SELECTED_TYPE_DEFAULT"
251
+ echo "imatrix: $IMATRIX_PATH"
252
+ echo "Output base: $OUTPUT_BASE_DIR"
253
+ echo "Split output: $SPLIT"
254
+
255
+ if [ "$DRY_RUN" = true ]; then
256
+ "$QUANTIZE_BIN" \
257
+ --dry-run \
258
+ --allow-requantize \
259
+ --tensor-type-file "$TENSOR_TYPE_FILE" \
260
+ --imatrix "$IMATRIX_PATH" \
261
+ "$INPUT_GGUF" \
262
+ "$INTERMEDIATE_OUTPUT" \
263
+ "$SELECTED_TYPE_DEFAULT"
264
+ else
265
+ "$QUANTIZE_BIN" \
266
+ --allow-requantize \
267
+ --tensor-type-file "$TENSOR_TYPE_FILE" \
268
+ --imatrix "$IMATRIX_PATH" \
269
+ "$INPUT_GGUF" \
270
+ "$INTERMEDIATE_OUTPUT" \
271
+ "$SELECTED_TYPE_DEFAULT"
272
+ fi
273
+
274
+ if [ "$DRY_RUN" = false ]; then
275
+ if [ "$SPLIT" = true ]; then
276
+ "$SPLIT_SCRIPT" \
277
+ "$LLAMA_CPP_DIR" \
278
+ "$INTERMEDIATE_OUTPUT" \
279
+ "$OUTPUT_DIR" \
280
+ "${MODEL_NAME}-${SELECTED_RECIPE_NAME}" \
281
+ --remove-input
282
+ else
283
+ echo "Quantization complete. Output saved to: $INTERMEDIATE_OUTPUT"
284
+ fi
285
+ fi
scripts/report-imatrix-activations.py ADDED
@@ -0,0 +1,661 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Report FFN activation importance from a llama.cpp imatrix GGUF.
3
+
4
+ The script ranks FFN down and gate/up activation streams from imatrix tensors.
5
+ For MoE tensors, it also tries to infer the routed expert count and report the
6
+ highest-activation block/expert pairs.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import math
13
+ import re
14
+ import sys
15
+ from collections import Counter, defaultdict
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+ from typing import Iterable
19
+
20
+ import numpy as np
21
+
22
+ try:
23
+ from gguf import GGUFReader
24
+ except ImportError as exc: # pragma: no cover - exercised by users without gguf.
25
+ raise SystemExit(
26
+ "Could not import gguf. Run this with a Python environment that has "
27
+ "llama.cpp's gguf package installed, for example:\n"
28
+ " ~/code/llama.cpp/.venv/bin/python scripts/report-imatrix-activations.py imatrix.gguf"
29
+ ) from exc
30
+
31
+
32
+ BLOCK_PATTERNS = (
33
+ re.compile(r"^blk\.(\d+)\.(.+)\.weight$"),
34
+ re.compile(r"^layers\.(\d+)\.(.+)\.weight$"),
35
+ re.compile(r"^model\.layers\.(\d+)\.(.+)\.weight$"),
36
+ )
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class Candidate:
41
+ base: str
42
+ tensor_name: str
43
+ count_name: str
44
+ block: int | None
45
+ module: str
46
+ kind: str
47
+ projection: str
48
+ family: str
49
+ n: int
50
+
51
+
52
+ @dataclass
53
+ class Row:
54
+ block: int | None
55
+ family: str
56
+ kind: str
57
+ projection: str
58
+ tensor: str
59
+ count: float
60
+ n: int
61
+ mean_mse: float
62
+ rms: float
63
+ p95: float
64
+ p99: float
65
+ max_value: float
66
+ gt1: float
67
+ gt4: float
68
+
69
+
70
+ @dataclass
71
+ class ExpertRow:
72
+ block: int
73
+ expert: int
74
+ family: str
75
+ tensor: str
76
+ count: float
77
+ n: int
78
+ mean_mse: float
79
+ rms: float
80
+ p95: float
81
+ p99: float
82
+ max_value: float
83
+
84
+
85
+ def parse_args() -> argparse.Namespace:
86
+ parser = argparse.ArgumentParser(
87
+ description="Rank FFN down and gate/up activation streams from an imatrix GGUF.",
88
+ )
89
+ parser.add_argument("imatrix", type=Path, help="Path to an imatrix .gguf file")
90
+ parser.add_argument(
91
+ "--top",
92
+ type=int,
93
+ default=15,
94
+ help="Rows to show in the top tensor-stream table. Default: 15",
95
+ )
96
+ parser.add_argument(
97
+ "--family-top",
98
+ type=int,
99
+ default=5,
100
+ help="Rows to show per tensor family. Default: 5",
101
+ )
102
+ parser.add_argument(
103
+ "--top-experts",
104
+ type=int,
105
+ default=25,
106
+ help="Rows to show in the combined routed expert table. Default: 25",
107
+ )
108
+ parser.add_argument(
109
+ "--top-expert-streams",
110
+ type=int,
111
+ default=15,
112
+ help="Rows to show in the per-stream routed expert table. Default: 15",
113
+ )
114
+ parser.add_argument(
115
+ "--last-blocks",
116
+ type=int,
117
+ default=10,
118
+ help="Number of final MoE blocks to show in the compact table. Default: 10",
119
+ )
120
+ parser.add_argument(
121
+ "--experts",
122
+ type=int,
123
+ default=None,
124
+ help="Override routed expert count. Useful when it cannot be inferred.",
125
+ )
126
+ parser.add_argument(
127
+ "--no-experts",
128
+ action="store_true",
129
+ help="Skip per-expert routed rankings.",
130
+ )
131
+ parser.add_argument(
132
+ "--include-router-input",
133
+ action="store_true",
134
+ help="Include ffn_gate_inp/router input tensors when present.",
135
+ )
136
+ return parser.parse_args()
137
+
138
+
139
+ def field_type_name(field: object, index: int) -> str:
140
+ try:
141
+ return field.types[index].name
142
+ except Exception:
143
+ return ""
144
+
145
+
146
+ def part_to_list(part: object) -> object:
147
+ return part.tolist() if hasattr(part, "tolist") else part
148
+
149
+
150
+ def decode_bytes(values: object) -> str:
151
+ if isinstance(values, list):
152
+ return bytes(values).decode("utf-8", errors="replace")
153
+ if isinstance(values, np.ndarray):
154
+ return bytes(values.tolist()).decode("utf-8", errors="replace")
155
+ if isinstance(values, bytes):
156
+ return values.decode("utf-8", errors="replace")
157
+ return str(values)
158
+
159
+
160
+ def decode_field(field: object) -> object:
161
+ """Best-effort conversion of a GGUF ReaderField into a printable value."""
162
+ if not getattr(field, "parts", None):
163
+ return ""
164
+
165
+ if field_type_name(field, 0) == "STRING":
166
+ return decode_bytes(part_to_list(field.parts[-1]))
167
+
168
+ if field_type_name(field, 0) == "ARRAY" and field_type_name(field, 1) == "STRING":
169
+ # llama.cpp imatrix files usually store a single dataset string.
170
+ return decode_bytes(part_to_list(field.parts[-1]))
171
+
172
+ value = part_to_list(field.parts[-1])
173
+ if isinstance(value, list) and len(value) == 1:
174
+ return value[0]
175
+ return value
176
+
177
+
178
+ def parse_block_module(base: str) -> tuple[int | None, str]:
179
+ for pattern in BLOCK_PATTERNS:
180
+ match = pattern.match(base)
181
+ if match:
182
+ return int(match.group(1)), match.group(2)
183
+ if base.endswith(".weight"):
184
+ return None, base[: -len(".weight")]
185
+ return None, base
186
+
187
+
188
+ def classify_module(module: str, include_router_input: bool = False) -> tuple[str, str, str] | None:
189
+ """Return (kind, projection, family) for FFN modules that this report handles."""
190
+ if module == "ffn_gate_inp" or module.endswith(".ffn_gate_inp"):
191
+ if not include_router_input:
192
+ return None
193
+ return "router", "gate input", "router gate input"
194
+
195
+ if "ffn_down" in module:
196
+ projection = "down"
197
+ elif "ffn_gate" in module or "ffn_up" in module:
198
+ projection = "gate/up"
199
+ else:
200
+ return None
201
+
202
+ if "_exps" in module or ".experts." in module or "_experts" in module:
203
+ kind = "routed expert"
204
+ elif "_shexp" in module or "shared_expert" in module or "shared_experts" in module:
205
+ kind = "shared expert"
206
+ else:
207
+ kind = "dense"
208
+
209
+ return kind, projection, f"{kind} {projection}"
210
+
211
+
212
+ def collect_lengths(reader: GGUFReader) -> dict[tuple[int | None, str], int]:
213
+ lengths: dict[tuple[int | None, str], int] = {}
214
+ for tensor in reader.tensors:
215
+ if not tensor.name.endswith(".in_sum2"):
216
+ continue
217
+ base = tensor.name[: -len(".in_sum2")]
218
+ block, module = parse_block_module(base)
219
+ lengths[(block, module)] = int(np.asarray(tensor.data).size)
220
+ return lengths
221
+
222
+
223
+ def collect_candidates(reader: GGUFReader, include_router_input: bool) -> list[Candidate]:
224
+ tensor_names = {tensor.name for tensor in reader.tensors}
225
+ candidates: list[Candidate] = []
226
+
227
+ for tensor in reader.tensors:
228
+ if not tensor.name.endswith(".in_sum2"):
229
+ continue
230
+
231
+ base = tensor.name[: -len(".in_sum2")]
232
+ count_name = base + ".counts"
233
+ if count_name not in tensor_names:
234
+ continue
235
+
236
+ block, module = parse_block_module(base)
237
+ classified = classify_module(module, include_router_input=include_router_input)
238
+ if classified is None:
239
+ continue
240
+
241
+ kind, projection, family = classified
242
+ candidates.append(
243
+ Candidate(
244
+ base=base,
245
+ tensor_name=tensor.name,
246
+ count_name=count_name,
247
+ block=block,
248
+ module=module,
249
+ kind=kind,
250
+ projection=projection,
251
+ family=family,
252
+ n=int(np.asarray(tensor.data).size),
253
+ )
254
+ )
255
+
256
+ return candidates
257
+
258
+
259
+ def infer_expert_count(candidates: Iterable[Candidate], lengths: dict[tuple[int | None, str], int]) -> tuple[int | None, Counter[int]]:
260
+ candidates = list(candidates)
261
+ gate_ref_modules = {
262
+ "attn_gate",
263
+ "attn_k",
264
+ "attn_q",
265
+ "attn_v",
266
+ "ffn_gate",
267
+ "ffn_up",
268
+ "ffn_gate_shexp",
269
+ "ffn_up_shexp",
270
+ }
271
+ down_ref_modules = {
272
+ "ffn_down",
273
+ "ffn_down_shexp",
274
+ }
275
+
276
+ gate_refs: dict[int | None, set[int]] = defaultdict(set)
277
+ down_refs: dict[int | None, set[int]] = defaultdict(set)
278
+ for (block, module), size in lengths.items():
279
+ if module in gate_ref_modules:
280
+ gate_refs[block].add(size)
281
+ if module in down_ref_modules:
282
+ down_refs[block].add(size)
283
+
284
+ # Prefer dimensions discovered from the FFN candidates themselves. This
285
+ # keeps inference useful for MoE GGUFs that have shared experts, dense
286
+ # leading layers, or non-standard attention tensor availability.
287
+ for candidate in candidates:
288
+ if candidate.block is None or candidate.kind == "routed expert":
289
+ continue
290
+ if candidate.projection == "gate/up":
291
+ gate_refs[candidate.block].add(candidate.n)
292
+ elif candidate.projection == "down":
293
+ down_refs[candidate.block].add(candidate.n)
294
+
295
+ votes: Counter[int] = Counter()
296
+ for candidate in candidates:
297
+ if candidate.kind != "routed expert" or candidate.block is None:
298
+ continue
299
+ refs = gate_refs[candidate.block] if candidate.projection == "gate/up" else down_refs[candidate.block]
300
+ for ref in refs:
301
+ if ref <= 0 or candidate.n % ref != 0:
302
+ continue
303
+ expert_count = candidate.n // ref
304
+ if 1 < expert_count <= 4096:
305
+ votes[expert_count] += 1
306
+
307
+ if not votes:
308
+ return None, votes
309
+ return votes.most_common(1)[0][0], votes
310
+
311
+
312
+ def deduplicate_gate_up(candidates: Iterable[Candidate]) -> list[Candidate]:
313
+ """Keep one tensor for each shared gate/up input stream."""
314
+ by_key: dict[tuple[int | None, str], Candidate] = {}
315
+ for candidate in candidates:
316
+ key = (candidate.block, candidate.family)
317
+ current = by_key.get(key)
318
+ if current is None:
319
+ by_key[key] = candidate
320
+ continue
321
+
322
+ # Prefer gate over up because it is the conventional representative.
323
+ if "ffn_gate" in candidate.module and "ffn_up" in current.module:
324
+ by_key[key] = candidate
325
+
326
+ return list(by_key.values())
327
+
328
+
329
+ def stats(values: np.ndarray) -> tuple[float, float, float, float, float, float, float]:
330
+ mean_mse = float(np.mean(values))
331
+ rms = math.sqrt(mean_mse) if mean_mse >= 0 and math.isfinite(mean_mse) else float("nan")
332
+ return (
333
+ mean_mse,
334
+ rms,
335
+ float(np.percentile(values, 95)),
336
+ float(np.percentile(values, 99)),
337
+ float(np.max(values)),
338
+ float(np.mean(values > 1.0)),
339
+ float(np.mean(values > 4.0)),
340
+ )
341
+
342
+
343
+ def analyze(reader: GGUFReader, args: argparse.Namespace) -> tuple[list[Row], list[ExpertRow], int | None, Counter[int]]:
344
+ tensor_by_name = {tensor.name: tensor for tensor in reader.tensors}
345
+ lengths = collect_lengths(reader)
346
+ candidates = collect_candidates(reader, include_router_input=args.include_router_input)
347
+ inferred_experts, expert_votes = infer_expert_count(candidates, lengths)
348
+ expert_count = args.experts if args.experts is not None else inferred_experts
349
+ candidates = deduplicate_gate_up(candidates)
350
+
351
+ rows: list[Row] = []
352
+ expert_rows: list[ExpertRow] = []
353
+
354
+ for candidate in candidates:
355
+ count = float(np.asarray(tensor_by_name[candidate.count_name].data).reshape(-1)[0])
356
+ raw = np.asarray(tensor_by_name[candidate.tensor_name].data, dtype=np.float64)
357
+ values = raw / count if count else np.full_like(raw, np.nan, dtype=np.float64)
358
+ mean_mse, rms, p95, p99, max_value, gt1, gt4 = stats(values)
359
+ rows.append(
360
+ Row(
361
+ block=candidate.block,
362
+ family=candidate.family,
363
+ kind=candidate.kind,
364
+ projection=candidate.projection,
365
+ tensor=candidate.base,
366
+ count=count,
367
+ n=candidate.n,
368
+ mean_mse=mean_mse,
369
+ rms=rms,
370
+ p95=p95,
371
+ p99=p99,
372
+ max_value=max_value,
373
+ gt1=gt1,
374
+ gt4=gt4,
375
+ )
376
+ )
377
+
378
+ if (
379
+ not args.no_experts
380
+ and expert_count
381
+ and candidate.kind == "routed expert"
382
+ and candidate.block is not None
383
+ and candidate.n % expert_count == 0
384
+ ):
385
+ per_expert = values.reshape(expert_count, candidate.n // expert_count)
386
+ for expert_id, expert_values in enumerate(per_expert):
387
+ exp_mean, exp_rms, exp_p95, exp_p99, exp_max, _, _ = stats(expert_values)
388
+ expert_rows.append(
389
+ ExpertRow(
390
+ block=candidate.block,
391
+ expert=expert_id,
392
+ family=candidate.family,
393
+ tensor=candidate.base,
394
+ count=count,
395
+ n=int(expert_values.size),
396
+ mean_mse=exp_mean,
397
+ rms=exp_rms,
398
+ p95=exp_p95,
399
+ p99=exp_p99,
400
+ max_value=exp_max,
401
+ )
402
+ )
403
+
404
+ return rows, expert_rows, expert_count, expert_votes
405
+
406
+
407
+ def fmt(value: object) -> str:
408
+ if isinstance(value, float):
409
+ if math.isnan(value):
410
+ return ""
411
+ return f"{value:.4g}"
412
+ if isinstance(value, int):
413
+ return str(value)
414
+ return str(value)
415
+
416
+
417
+ def pct(value: float) -> str:
418
+ return f"{100.0 * value:.1f}%"
419
+
420
+
421
+ def block_label(block: int | None) -> str:
422
+ return "?" if block is None else str(block)
423
+
424
+
425
+ def print_table(headers: list[str], rows: Iterable[list[object]]) -> None:
426
+ print("| " + " | ".join(headers) + " |")
427
+ print("| " + " | ".join("---" for _ in headers) + " |")
428
+ for row in rows:
429
+ print("| " + " | ".join(fmt(value) for value in row) + " |")
430
+
431
+
432
+ def metadata(reader: GGUFReader) -> dict[str, object]:
433
+ wanted = ("general.type", "imatrix.datasets", "imatrix.chunk_count", "imatrix.chunk_size")
434
+ return {key: decode_field(reader.fields[key]) for key in wanted if key in reader.fields}
435
+
436
+
437
+ def aggregate_experts(expert_rows: Iterable[ExpertRow]) -> list[dict[str, object]]:
438
+ acc: dict[tuple[int, int], dict[str, object]] = {}
439
+ for row in expert_rows:
440
+ key = (row.block, row.expert)
441
+ item = acc.setdefault(
442
+ key,
443
+ {
444
+ "block": row.block,
445
+ "expert": row.expert,
446
+ "weighted_mse_sum": 0.0,
447
+ "n": 0,
448
+ "max_stream_rms": 0.0,
449
+ "streams": [],
450
+ },
451
+ )
452
+ item["weighted_mse_sum"] = float(item["weighted_mse_sum"]) + row.mean_mse * row.n
453
+ item["n"] = int(item["n"]) + row.n
454
+ item["max_stream_rms"] = max(float(item["max_stream_rms"]), row.rms)
455
+ item["streams"].append(row.family)
456
+
457
+ result: list[dict[str, object]] = []
458
+ for item in acc.values():
459
+ mean_mse = float(item["weighted_mse_sum"]) / int(item["n"])
460
+ result.append(
461
+ {
462
+ "block": item["block"],
463
+ "expert": item["expert"],
464
+ "rms": math.sqrt(mean_mse),
465
+ "mean_mse": mean_mse,
466
+ "max_stream_rms": item["max_stream_rms"],
467
+ "streams": ", ".join(sorted(set(item["streams"]))),
468
+ }
469
+ )
470
+ return result
471
+
472
+
473
+ def render(path: Path, reader: GGUFReader, rows: list[Row], expert_rows: list[ExpertRow], expert_count: int | None, expert_votes: Counter[int], args: argparse.Namespace) -> None:
474
+ print(f"# {path.name}")
475
+ print()
476
+ print("Score: `RMS = sqrt(mean(in_sum2 / counts))`.")
477
+ print()
478
+
479
+ meta = metadata(reader)
480
+ if meta:
481
+ print("Metadata:")
482
+ print()
483
+ print_table(["Key", "Value"], ([key, value] for key, value in meta.items()))
484
+ print()
485
+
486
+ blocks = sorted(row.block for row in rows if row.block is not None)
487
+ if blocks:
488
+ print(f"Blocks covered: `{blocks[0]}-{blocks[-1]}`")
489
+ print()
490
+
491
+ if expert_count:
492
+ source = "command line" if args.experts is not None else "inferred from tensor lengths"
493
+ print(f"Routed expert count: `{expert_count}` ({source}).")
494
+ if expert_votes and args.experts is None:
495
+ vote_text = ", ".join(f"{count}: {votes}" for count, votes in expert_votes.most_common(5))
496
+ print(f"Expert-count inference votes: `{vote_text}`.")
497
+ print()
498
+ elif not args.no_experts:
499
+ print("Routed expert count could not be inferred. Use `--experts N` to enable per-expert rankings.")
500
+ print()
501
+
502
+ rows_sorted = sorted(rows, key=lambda row: row.rms, reverse=True)
503
+ if rows_sorted:
504
+ print(f"Top {min(args.top, len(rows_sorted))} `down` / `gate/up` tensor streams:")
505
+ print()
506
+ print_table(
507
+ ["Block", "Family", "Tensor", "RMS", "Mean MSE", "P95", "Max", "Count"],
508
+ (
509
+ [
510
+ block_label(row.block),
511
+ row.family,
512
+ f"`{row.tensor}`",
513
+ row.rms,
514
+ row.mean_mse,
515
+ row.p95,
516
+ row.max_value,
517
+ int(row.count),
518
+ ]
519
+ for row in rows_sorted[: args.top]
520
+ ),
521
+ )
522
+ print()
523
+
524
+ if expert_rows:
525
+ print(
526
+ "Per-expert rankings assume llama.cpp-style routed expert tensors are flattened "
527
+ "with each expert stored contiguously. Counts are tensor-level counts, so these "
528
+ "rank aggregate activation contribution per expert stream."
529
+ )
530
+ print()
531
+
532
+ aggregated = sorted(aggregate_experts(expert_rows), key=lambda row: float(row["rms"]), reverse=True)
533
+ print(f"Top {min(args.top_experts, len(aggregated))} routed experts, combined across routed down and gate/up:")
534
+ print()
535
+ print_table(
536
+ ["Block", "Expert", "Combined RMS", "Mean MSE", "Max stream RMS", "Streams"],
537
+ (
538
+ [
539
+ row["block"],
540
+ row["expert"],
541
+ row["rms"],
542
+ row["mean_mse"],
543
+ row["max_stream_rms"],
544
+ row["streams"],
545
+ ]
546
+ for row in aggregated[: args.top_experts]
547
+ ),
548
+ )
549
+ print()
550
+
551
+ expert_streams = sorted(expert_rows, key=lambda row: row.rms, reverse=True)
552
+ print(f"Top {min(args.top_expert_streams, len(expert_streams))} routed expert streams:")
553
+ print()
554
+ print_table(
555
+ ["Block", "Expert", "Family", "Tensor", "RMS", "Mean MSE", "P95", "Max", "Count"],
556
+ (
557
+ [
558
+ row.block,
559
+ row.expert,
560
+ row.family,
561
+ f"`{row.tensor}`",
562
+ row.rms,
563
+ row.mean_mse,
564
+ row.p95,
565
+ row.max_value,
566
+ int(row.count),
567
+ ]
568
+ for row in expert_streams[: args.top_expert_streams]
569
+ ),
570
+ )
571
+ print()
572
+
573
+ families = [
574
+ "dense down",
575
+ "dense gate/up",
576
+ "routed expert down",
577
+ "routed expert gate/up",
578
+ "shared expert down",
579
+ "shared expert gate/up",
580
+ "router gate input",
581
+ ]
582
+ print(f"Top {args.family_top} by family:")
583
+ print()
584
+ family_rows: list[list[object]] = []
585
+ for family in families:
586
+ subset = [row for row in rows if row.family == family]
587
+ for row in sorted(subset, key=lambda item: item.rms, reverse=True)[: args.family_top]:
588
+ family_rows.append(
589
+ [
590
+ family,
591
+ block_label(row.block),
592
+ f"`{row.tensor}`",
593
+ row.rms,
594
+ row.mean_mse,
595
+ row.p95,
596
+ row.p99,
597
+ row.max_value,
598
+ pct(row.gt1),
599
+ pct(row.gt4),
600
+ int(row.count),
601
+ ]
602
+ )
603
+ print_table(
604
+ ["Family", "Block", "Tensor", "RMS", "Mean MSE", "P95", "P99", "Max", ">1", ">4", "Count"],
605
+ family_rows,
606
+ )
607
+ print()
608
+
609
+ moe_blocks = sorted(
610
+ {
611
+ row.block
612
+ for row in rows
613
+ if row.block is not None and row.kind in {"routed expert", "shared expert"}
614
+ }
615
+ )
616
+ if args.last_blocks > 0 and moe_blocks:
617
+ selected_blocks = moe_blocks[-args.last_blocks :]
618
+ by_block_family = {(row.block, row.family): row for row in rows}
619
+ print(f"Last {len(selected_blocks)} MoE block RMS:")
620
+ print()
621
+ compact_rows: list[list[object]] = []
622
+ for block in selected_blocks:
623
+ compact_rows.append(
624
+ [
625
+ block,
626
+ by_block_family.get((block, "shared expert down"), "").rms
627
+ if (block, "shared expert down") in by_block_family
628
+ else "",
629
+ by_block_family.get((block, "shared expert gate/up"), "").rms
630
+ if (block, "shared expert gate/up") in by_block_family
631
+ else "",
632
+ by_block_family.get((block, "routed expert down"), "").rms
633
+ if (block, "routed expert down") in by_block_family
634
+ else "",
635
+ by_block_family.get((block, "routed expert gate/up"), "").rms
636
+ if (block, "routed expert gate/up") in by_block_family
637
+ else "",
638
+ ]
639
+ )
640
+ print_table(["Block", "Shared down", "Shared gate/up", "Routed down", "Routed gate/up"], compact_rows)
641
+ print()
642
+
643
+
644
+ def main() -> int:
645
+ args = parse_args()
646
+ if not args.imatrix.exists():
647
+ print(f"error: file not found: {args.imatrix}", file=sys.stderr)
648
+ return 2
649
+
650
+ reader = GGUFReader(str(args.imatrix))
651
+ rows, expert_rows, expert_count, expert_votes = analyze(reader, args)
652
+ if not rows:
653
+ print("error: no FFN down/gate/up imatrix tensors were found", file=sys.stderr)
654
+ return 1
655
+
656
+ render(args.imatrix, reader, rows, expert_rows, expert_count, expert_votes, args)
657
+ return 0
658
+
659
+
660
+ if __name__ == "__main__":
661
+ raise SystemExit(main())
scripts/split.sh ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ usage() {
6
+ cat <<'EOF'
7
+ Usage: split.sh <llama_cpp_dir> <input_gguf> <output_dir> [output_prefix] [--remove-input]
8
+
9
+ Environment overrides:
10
+ SPLIT_MAX_SIZE=50G Maximum size for each split file.
11
+
12
+ Examples:
13
+ ./scripts/split.sh ~/code/llama.cpp model.gguf ./Q5_K_S
14
+ ./scripts/split.sh ~/code/llama.cpp model.gguf ./Q5_K_S model-Q5_K_S --remove-input
15
+ EOF
16
+ }
17
+
18
+ if [ $# -lt 3 ]; then
19
+ usage
20
+ exit 1
21
+ fi
22
+
23
+ LLAMA_CPP_DIR="$1"
24
+ INPUT_GGUF="$2"
25
+ OUTPUT_DIR="$3"
26
+ OUTPUT_PREFIX_NAME=
27
+ REMOVE_INPUT=false
28
+ shift 3
29
+
30
+ while [ $# -gt 0 ]; do
31
+ case "$1" in
32
+ --remove-input)
33
+ REMOVE_INPUT=true
34
+ ;;
35
+ --*)
36
+ usage
37
+ exit 1
38
+ ;;
39
+ *)
40
+ if [ -n "$OUTPUT_PREFIX_NAME" ]; then
41
+ usage
42
+ exit 1
43
+ fi
44
+ OUTPUT_PREFIX_NAME="$1"
45
+ ;;
46
+ esac
47
+ shift
48
+ done
49
+
50
+ if [ ! -d "$LLAMA_CPP_DIR" ]; then
51
+ echo "Error: llama.cpp directory not found: $LLAMA_CPP_DIR" >&2
52
+ exit 1
53
+ fi
54
+
55
+ if [ ! -e "$INPUT_GGUF" ]; then
56
+ echo "Error: input GGUF not found: $INPUT_GGUF" >&2
57
+ exit 1
58
+ fi
59
+
60
+ SPLIT_BIN="$LLAMA_CPP_DIR/build/bin/llama-gguf-split"
61
+ if [ ! -x "$SPLIT_BIN" ]; then
62
+ echo "Error: llama-gguf-split binary not found: $SPLIT_BIN" >&2
63
+ exit 1
64
+ fi
65
+
66
+ if [ -z "$OUTPUT_PREFIX_NAME" ]; then
67
+ input_basename="$(basename "$INPUT_GGUF")"
68
+ OUTPUT_PREFIX_NAME="${input_basename%.gguf}"
69
+ fi
70
+
71
+ SPLIT_MAX_SIZE="${SPLIT_MAX_SIZE:-50G}"
72
+ mkdir -p "$OUTPUT_DIR"
73
+ OUTPUT_PREFIX="$OUTPUT_DIR/$OUTPUT_PREFIX_NAME"
74
+
75
+ "$SPLIT_BIN" \
76
+ --split-max-size "$SPLIT_MAX_SIZE" \
77
+ --no-tensor-first-split \
78
+ "$INPUT_GGUF" \
79
+ "$OUTPUT_PREFIX"
80
+
81
+ if [ "$REMOVE_INPUT" = true ]; then
82
+ rm -f "$INPUT_GGUF"
83
+ fi
84
+
85
+ echo "Split complete. Output saved to: $OUTPUT_DIR"