summaryrefslogtreecommitdiff
path: root/llama.cpp/gguf-py/gguf/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'llama.cpp/gguf-py/gguf/scripts')
-rwxr-xr-xllama.cpp/gguf-py/gguf/scripts/gguf_convert_endian.py186
-rwxr-xr-xllama.cpp/gguf-py/gguf/scripts/gguf_dump.py477
-rwxr-xr-xllama.cpp/gguf-py/gguf/scripts/gguf_editor_gui.py1621
-rwxr-xr-xllama.cpp/gguf-py/gguf/scripts/gguf_hash.py102
-rwxr-xr-xllama.cpp/gguf-py/gguf/scripts/gguf_new_metadata.py216
-rwxr-xr-xllama.cpp/gguf-py/gguf/scripts/gguf_set_metadata.py95
6 files changed, 2697 insertions, 0 deletions
diff --git a/llama.cpp/gguf-py/gguf/scripts/gguf_convert_endian.py b/llama.cpp/gguf-py/gguf/scripts/gguf_convert_endian.py
new file mode 100755
index 0000000..86bf878
--- /dev/null
+++ b/llama.cpp/gguf-py/gguf/scripts/gguf_convert_endian.py
@@ -0,0 +1,186 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+import logging
+import argparse
+import os
+import sys
+from tqdm import tqdm
+from pathlib import Path
+
+import numpy as np
+
+# Necessary to load the local gguf package
+if "NO_LOCAL_GGUF" not in os.environ and (Path(__file__).parent.parent.parent.parent / 'gguf-py').exists():
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
+
+import gguf
+
+logger = logging.getLogger("gguf-convert-endian")
+
+
+def byteswap_noop(tensor, block_offs):
+ # this function is used when byteswapping is not needed
+ pass
+
+
+def byteswap_q4_0(tensor, block_offs):
+ # Each block_q4_0 consists of an f16 delta (scaling factor) followed by 16 int8 quantizations.
+
+ # Byte-Swap f16 sized delta field
+ delta = tensor.data[block_offs:block_offs + 2].view(dtype=np.uint16)
+ delta.byteswap(inplace=True)
+
+
+def byteswap_q8_0(tensor, block_offs):
+ # Each block_q8_0 consists of an f16 delta (scaling factor) followed by 32 int8 quantizations.
+
+ # Byte-Swap f16 sized delta field
+ delta = tensor.data[block_offs:block_offs + 2].view(dtype=np.uint16)
+ delta.byteswap(inplace=True)
+
+
+def byteswap_q4_k(tensor, block_offs):
+ # Each block_q4_k consists of 2 f16 values followed by 140 int8 values.
+
+ # Byte-Swap f16 sized fields
+ delta = tensor.data[block_offs:block_offs + 2].view(dtype=np.uint16)
+ delta.byteswap(inplace=True)
+
+ delta = tensor.data[block_offs + 2:block_offs + 4].view(dtype=np.uint16)
+ delta.byteswap(inplace=True)
+
+
+def byteswap_q6_k(tensor, block_offs):
+ # Each block_q6_k consists of 208 int8 values followed by 1 f16 value.
+
+ # Byte-Swap f16 sized field
+ delta = tensor.data[block_offs + 208:block_offs + 210].view(dtype=np.uint16)
+ delta.byteswap(inplace=True)
+
+
+byteswap_tensors = {
+ gguf.GGMLQuantizationType.Q4_0: byteswap_q4_0,
+ gguf.GGMLQuantizationType.Q8_0: byteswap_q8_0,
+ gguf.GGMLQuantizationType.Q4_K: byteswap_q4_k,
+ gguf.GGMLQuantizationType.Q6_K: byteswap_q6_k,
+ gguf.GGMLQuantizationType.MXFP4: byteswap_noop,
+}
+
+
+def convert_byteorder(reader: gguf.GGUFReader, args: argparse.Namespace) -> None:
+ file_endian = reader.endianess.name
+ if reader.byte_order == 'S':
+ host_endian = 'BIG' if file_endian == 'LITTLE' else 'LITTLE'
+ else:
+ host_endian = file_endian
+ order = host_endian if args.order == "native" else args.order.upper()
+ logger.info(f"* Host is {host_endian} endian, GGUF file seems to be {file_endian} endian")
+ if file_endian == order:
+ logger.info(f"* File is already {order} endian. Nothing to do.")
+ sys.exit(0)
+ logger.info("* Checking tensors for conversion compatibility")
+ for tensor in reader.tensors:
+ if tensor.tensor_type not in byteswap_tensors and \
+ tensor.tensor_type not in (
+ gguf.GGMLQuantizationType.F32,
+ gguf.GGMLQuantizationType.F16,
+ gguf.GGMLQuantizationType.BF16,
+ ):
+ raise ValueError(f"Cannot handle type {tensor.tensor_type.name} for tensor {repr(tensor.name)}")
+ logger.info(f"* Preparing to convert from {file_endian} to {order}")
+ if args.dry_run:
+ return
+ logger.warning("*** Warning *** Warning *** Warning **")
+ logger.warning("* This conversion process may damage the file. Ensure you have a backup.")
+ if order != host_endian:
+ logger.warning("* Requested endian differs from host, you will not be able to load the model on this machine.")
+ logger.warning("* The file will be modified immediately, so if conversion fails or is interrupted")
+ logger.warning("* the file will be corrupted. Enter exactly YES if you are positive you want to proceed:")
+ response = input("YES, I am sure> ")
+ if response != "YES":
+ logger.warning("You didn't enter YES. Okay then, see ya!")
+ sys.exit(0)
+ logger.info(f"* Converting fields ({len(reader.fields)})")
+ for idx, field in enumerate(reader.fields.values()):
+ logger.info(f"- {idx:4}: Converting field {repr(field.name)}, part count: {len(field.parts)}")
+ for part in field.parts:
+ part.byteswap(inplace=True)
+ logger.info(f"* Converting tensors ({len(reader.tensors)})")
+
+ for idx, tensor in enumerate(pbar := tqdm(reader.tensors, desc="Converting tensor")):
+ log_message = (
+ f"Converting tensor {repr(tensor.name)}, "
+ f"type={tensor.tensor_type.name}, "
+ f"elements={tensor.n_elements} "
+ )
+
+ # Byte-swap each part of the tensor's field
+ for part in tensor.field.parts:
+ part.byteswap(inplace=True)
+
+ # Byte-swap tensor data if necessary
+ if tensor.tensor_type in byteswap_tensors:
+ # first flatten structure
+ oldshape = tensor.data.shape
+ newshape = 1
+ for i in tensor.data.shape:
+ newshape *= i
+
+ tensor.data.resize(newshape)
+
+ block_size = gguf.constants.GGML_QUANT_SIZES[tensor.tensor_type][1]
+ byteswap_func = byteswap_tensors[tensor.tensor_type]
+
+ n_blocks = len(tensor.data) // block_size
+ for block_num in (inner_pbar := tqdm(range(n_blocks), desc="Byte-swapping Blocks", leave=False)):
+ block_offs = block_num * block_size
+
+ byteswap_func(tensor, block_offs)
+
+ if block_num % 100000 == 0:
+ inner_pbar.set_description(f"Byte-swapping Blocks [{(n_blocks - block_num) // n_blocks}]")
+
+ # restore old shape in case it's ever used
+ tensor.data.resize(oldshape)
+ elif tensor.tensor_type == gguf.GGMLQuantizationType.BF16:
+ # Special case for BF16
+ # It is 2-bytes data, but by default view loads it as 1-byte data.
+ # Change to correct view before byteswapping.
+ tensor.data.view(dtype=np.uint16).byteswap(inplace=True)
+ else:
+ # Handle other tensor types
+ tensor.data.byteswap(inplace=True)
+
+ pbar.set_description(log_message)
+
+ logger.info("* Completion")
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description="Convert GGUF file byte order")
+ parser.add_argument(
+ "model", type=str,
+ help="GGUF format model filename",
+ )
+ parser.add_argument(
+ "order", type=str, choices=['big', 'little', 'native'],
+ help="Requested byte order",
+ )
+ parser.add_argument(
+ "--dry-run", action="store_true",
+ help="Don't actually change anything",
+ )
+ parser.add_argument("--verbose", action="store_true", help="increase output verbosity")
+
+ args = parser.parse_args(None if len(sys.argv) > 1 else ["--help"])
+
+ logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
+
+ logger.info(f'* Loading: {args.model}')
+ reader = gguf.GGUFReader(args.model, 'r' if args.dry_run else 'r+')
+ convert_byteorder(reader, args)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/llama.cpp/gguf-py/gguf/scripts/gguf_dump.py b/llama.cpp/gguf-py/gguf/scripts/gguf_dump.py
new file mode 100755
index 0000000..8177dff
--- /dev/null
+++ b/llama.cpp/gguf-py/gguf/scripts/gguf_dump.py
@@ -0,0 +1,477 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+import logging
+import argparse
+import os
+import re
+import sys
+from pathlib import Path
+from typing import Any
+
+# Necessary to load the local gguf package
+if "NO_LOCAL_GGUF" not in os.environ and (Path(__file__).parent.parent.parent.parent / 'gguf-py').exists():
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
+
+from gguf import GGUFReader, GGUFValueType, ReaderTensor # noqa: E402
+
+logger = logging.getLogger("gguf-dump")
+
+
+def get_file_host_endian(reader: GGUFReader) -> tuple[str, str]:
+ file_endian = reader.endianess.name
+ if reader.byte_order == 'S':
+ host_endian = 'BIG' if file_endian == 'LITTLE' else 'LITTLE'
+ else:
+ host_endian = file_endian
+ return (host_endian, file_endian)
+
+
+# For more information about what field.parts and field.data represent,
+# please see the comments in the modify_gguf.py example.
+def dump_metadata(reader: GGUFReader, args: argparse.Namespace) -> None:
+ host_endian, file_endian = get_file_host_endian(reader)
+ print(f'* File is {file_endian} endian, script is running on a {host_endian} endian host.') # noqa: NP100
+ print(f'* Dumping {len(reader.fields)} key/value pair(s)') # noqa: NP100
+ for n, field in enumerate(reader.fields.values(), 1):
+ if not field.types:
+ pretty_type = 'N/A'
+ elif field.types[0] == GGUFValueType.ARRAY:
+ nest_count = len(field.types) - 1
+ pretty_type = '[' * nest_count + str(field.types[-1].name) + ']' * nest_count
+ else:
+ pretty_type = str(field.types[-1].name)
+
+ log_message = f' {n:5}: {pretty_type:10} | {len(field.data):8} | {field.name}'
+ if field.types:
+ curr_type = field.types[0]
+ if curr_type == GGUFValueType.STRING:
+ content = field.contents()
+ if len(content) > 60:
+ content = content[:57] + '...'
+ log_message += ' = {0}'.format(repr(content))
+ elif curr_type in reader.gguf_scalar_to_np:
+ log_message += ' = {0}'.format(field.contents())
+ else:
+ content = repr(field.contents(slice(6)))
+ if len(field.data) > 6:
+ content = content[:-1] + ', ...]'
+ log_message += ' = {0}'.format(content)
+ print(log_message) # noqa: NP100
+ if args.no_tensors:
+ return
+ print(f'* Dumping {len(reader.tensors)} tensor(s)') # noqa: NP100
+ for n, tensor in enumerate(reader.tensors, 1):
+ prettydims = ', '.join('{0:5}'.format(d) for d in list(tensor.shape) + [1] * (4 - len(tensor.shape)))
+ print(f' {n:5}: {tensor.n_elements:10} | {prettydims} | {tensor.tensor_type.name:7} | {tensor.name}') # noqa: NP100
+
+
+def dump_metadata_json(reader: GGUFReader, args: argparse.Namespace) -> None:
+ import json
+ host_endian, file_endian = get_file_host_endian(reader)
+ metadata: dict[str, Any] = {}
+ tensors: dict[str, Any] = {}
+ result = {
+ "filename": args.model,
+ "endian": file_endian,
+ "metadata": metadata,
+ "tensors": tensors,
+ }
+ for idx, field in enumerate(reader.fields.values()):
+ curr: dict[str, Any] = {
+ "index": idx,
+ "type": field.types[0].name if field.types else 'UNKNOWN',
+ "offset": field.offset,
+ }
+ metadata[field.name] = curr
+ if field.types[:1] == [GGUFValueType.ARRAY]:
+ curr["array_types"] = [t.name for t in field.types][1:]
+ if not args.json_array:
+ continue
+ curr["value"] = field.contents()
+ else:
+ curr["value"] = field.contents()
+ if not args.no_tensors:
+ for idx, tensor in enumerate(reader.tensors):
+ tensors[tensor.name] = {
+ "index": idx,
+ "shape": tensor.shape.tolist(),
+ "type": tensor.tensor_type.name,
+ "offset": tensor.field.offset,
+ }
+ json.dump(result, sys.stdout)
+
+
+def markdown_table_with_alignment_support(header_map: list[dict[str, str]], data: list[dict[str, Any]]):
+ # JSON to Markdown table formatting: https://stackoverflow.com/a/72983854/2850957
+
+ # Alignment Utility Function
+ def strAlign(padding: int, alignMode: str | None, strVal: str):
+ if alignMode == 'center':
+ return strVal.center(padding)
+ elif alignMode == 'right':
+ return strVal.rjust(padding - 1) + ' '
+ elif alignMode == 'left':
+ return ' ' + strVal.ljust(padding - 1)
+ else: # default left
+ return ' ' + strVal.ljust(padding - 1)
+
+ def dashAlign(padding: int, alignMode: str | None):
+ if alignMode == 'center':
+ return ':' + '-' * (padding - 2) + ':'
+ elif alignMode == 'right':
+ return '-' * (padding - 1) + ':'
+ elif alignMode == 'left':
+ return ':' + '-' * (padding - 1)
+ else: # default left
+ return '-' * (padding)
+
+ # Calculate Padding For Each Column Based On Header and Data Length
+ rowsPadding = {}
+ for index, columnEntry in enumerate(header_map):
+ padCount = max([len(str(v)) for d in data for k, v in d.items() if k == columnEntry['key_name']], default=0) + 2
+ headerPadCount = len(columnEntry['header_name']) + 2
+ rowsPadding[index] = headerPadCount if padCount <= headerPadCount else padCount
+
+ # Render Markdown Header
+ rows = []
+ rows.append('|'.join(strAlign(rowsPadding[index], columnEntry.get('align'), str(columnEntry['header_name'])) for index, columnEntry in enumerate(header_map)))
+ rows.append('|'.join(dashAlign(rowsPadding[index], columnEntry.get('align')) for index, columnEntry in enumerate(header_map)))
+
+ # Render Tabular Data
+ for item in data:
+ rows.append('|'.join(strAlign(rowsPadding[index], columnEntry.get('align'), str(item[columnEntry['key_name']])) for index, columnEntry in enumerate(header_map)))
+
+ # Convert Tabular String Rows Into String
+ tableString = ""
+ for row in rows:
+ tableString += f'|{row}|\n'
+
+ return tableString
+
+
+def element_count_rounded_notation(count: int) -> str:
+ if count > 1e15 :
+ # Quadrillion
+ scaled_amount = count * 1e-15
+ scale_suffix = "Q"
+ elif count > 1e12 :
+ # Trillions
+ scaled_amount = count * 1e-12
+ scale_suffix = "T"
+ elif count > 1e9 :
+ # Billions
+ scaled_amount = count * 1e-9
+ scale_suffix = "B"
+ elif count > 1e6 :
+ # Millions
+ scaled_amount = count * 1e-6
+ scale_suffix = "M"
+ elif count > 1e3 :
+ # Thousands
+ scaled_amount = count * 1e-3
+ scale_suffix = "K"
+ else:
+ # Under Thousands
+ scaled_amount = count
+ scale_suffix = ""
+ return f"{'~' if count > 1e3 else ''}{round(scaled_amount)}{scale_suffix}"
+
+
+def translate_tensor_name(name):
+ words = name.split(".")
+
+ # Source: https://github.com/ggml-org/ggml/blob/master/docs/gguf.md#standardized-tensor-names
+ abbreviation_dictionary = {
+ 'token_embd': 'Token embedding',
+ 'pos_embd': 'Position embedding',
+ 'output_norm': 'Output normalization',
+ 'output': 'Output',
+ 'attn_norm': 'Attention normalization',
+ 'attn_norm_2': 'Attention normalization',
+ 'attn_qkv': 'Attention query-key-value',
+ 'attn_q': 'Attention query',
+ 'attn_k': 'Attention key',
+ 'attn_v': 'Attention value',
+ 'attn_output': 'Attention output',
+ 'ffn_norm': 'Feed-forward network normalization',
+ 'ffn_up': 'Feed-forward network "up"',
+ 'ffn_gate': 'Feed-forward network "gate"',
+ 'ffn_down': 'Feed-forward network "down"',
+ 'ffn_gate_inp': 'Expert-routing layer for the Feed-forward network in Mixture of Expert models',
+ 'ffn_gate_exp': 'Feed-forward network "gate" layer per expert in Mixture of Expert models',
+ 'ffn_down_exp': 'Feed-forward network "down" layer per expert in Mixture of Expert models',
+ 'ffn_up_exp': 'Feed-forward network "up" layer per expert in Mixture of Expert models',
+ 'ssm_in': 'State space model input projections',
+ 'ssm_conv1d': 'State space model rolling/shift',
+ 'ssm_x': 'State space model selective parametrization',
+ 'ssm_a': 'State space model state compression',
+ 'ssm_d': 'State space model skip connection',
+ 'ssm_dt': 'State space model time step',
+ 'ssm_out': 'State space model output projection',
+ 'blk': 'Block',
+ 'enc': 'Encoder',
+ 'dec': 'Decoder',
+ }
+
+ expanded_words = []
+ for word in words:
+ word_norm = word.strip().lower()
+ if word_norm in abbreviation_dictionary:
+ expanded_words.append(abbreviation_dictionary[word_norm].title())
+ else:
+ expanded_words.append(word.title())
+
+ return ' '.join(expanded_words)
+
+
+def dump_markdown_metadata(reader: GGUFReader, args: argparse.Namespace) -> None:
+ host_endian, file_endian = get_file_host_endian(reader)
+ markdown_content = ""
+ markdown_content += f'# {args.model} - GGUF Internal File Dump\n\n'
+ markdown_content += f'- Endian: {file_endian} endian\n'
+ markdown_content += '\n'
+ markdown_content += '## Key Value Metadata Store\n\n'
+ markdown_content += f'There are {len(reader.fields)} key-value pairs in this file\n'
+ markdown_content += '\n'
+ total_model_bytes = 0
+ total_model_elements = 0
+
+ kv_dump_table: list[dict[str, str | int]] = []
+ for n, field in enumerate(reader.fields.values(), 1):
+ if not field.types:
+ pretty_type = 'N/A'
+ elif field.types[0] == GGUFValueType.ARRAY:
+ nest_count = len(field.types) - 1
+ pretty_type = '[' * nest_count + str(field.types[-1].name) + ']' * nest_count
+ else:
+ pretty_type = str(field.types[-1].name)
+
+ def escape_markdown_inline_code(value_string):
+ # Find the longest contiguous sequence of backticks in the string then
+ # wrap string with appropriate number of backticks required to escape it
+ max_backticks = max((len(match.group(0)) for match in re.finditer(r'`+', value_string)), default=0)
+ inline_code_marker = '`' * (max_backticks + 1)
+
+ # If the string starts or ends with a backtick, add a space at the beginning and end
+ if value_string.startswith('`') or value_string.endswith('`'):
+ value_string = f" {value_string} "
+
+ return f"{inline_code_marker}{value_string}{inline_code_marker}"
+
+ total_elements = len(field.data)
+ value = ""
+ if len(field.types) == 1:
+ curr_type = field.types[0]
+ if curr_type == GGUFValueType.STRING:
+ truncate_length = 60
+ value_string = str(bytes(field.parts[-1]), encoding='utf-8')
+ if len(value_string) > truncate_length:
+ head = escape_markdown_inline_code(value_string[:truncate_length // 2])
+ tail = escape_markdown_inline_code(value_string[-truncate_length // 2:])
+ value = "{head}...{tail}".format(head=head, tail=tail)
+ else:
+ value = escape_markdown_inline_code(value_string)
+ elif curr_type in reader.gguf_scalar_to_np:
+ value = str(field.parts[-1][0])
+ else:
+ if field.types[0] == GGUFValueType.ARRAY:
+ curr_type = field.types[1]
+ array_elements = []
+
+ if curr_type == GGUFValueType.STRING:
+ render_element = min(5, total_elements)
+ for element_pos in range(render_element):
+ truncate_length = 30
+ value_string = str(bytes(field.parts[-1 - (total_elements - element_pos - 1) * 2]), encoding='utf-8')
+ if len(value_string) > truncate_length:
+ head = escape_markdown_inline_code(value_string[:truncate_length // 2])
+ tail = escape_markdown_inline_code(value_string[-truncate_length // 2:])
+ value = "{head}...{tail}".format(head=head, tail=tail)
+ else:
+ value = escape_markdown_inline_code(value_string)
+ array_elements.append(value)
+
+ elif curr_type in reader.gguf_scalar_to_np:
+ render_element = min(7, total_elements)
+ for element_pos in range(render_element):
+ array_elements.append(str(field.parts[-1 - (total_elements - element_pos - 1)][0]))
+
+ value = f'[ {", ".join(array_elements).strip()}{", ..." if total_elements > len(array_elements) else ""} ]'
+
+ kv_dump_table.append({"n":n, "pretty_type":pretty_type, "total_elements":total_elements, "field_name":field.name, "value":value})
+
+ kv_dump_table_header_map = [
+ {'key_name':'n', 'header_name':'POS', 'align':'right'},
+ {'key_name':'pretty_type', 'header_name':'TYPE', 'align':'left'},
+ {'key_name':'total_elements', 'header_name':'Count', 'align':'right'},
+ {'key_name':'field_name', 'header_name':'Key', 'align':'left'},
+ {'key_name':'value', 'header_name':'Value', 'align':'left'},
+ ]
+
+ markdown_content += markdown_table_with_alignment_support(kv_dump_table_header_map, kv_dump_table)
+
+ markdown_content += "\n"
+
+ if not args.no_tensors:
+ # Group tensors by their prefix and maintain order
+ tensor_prefix_order: list[str] = []
+ tensor_name_to_key: dict[str, int] = {}
+ tensor_groups: dict[str, list[ReaderTensor]] = {}
+ total_elements = sum(tensor.n_elements for tensor in reader.tensors)
+
+ # Parsing Tensors Record
+ for key, tensor in enumerate(reader.tensors):
+ tensor_components = tensor.name.split('.')
+
+ # Classify Tensor Group
+ tensor_group_name = "base"
+ if tensor_components[0] == 'blk':
+ tensor_group_name = f"{tensor_components[0]}.{tensor_components[1]}"
+ elif tensor_components[0] in ['enc', 'dec'] and tensor_components[1] == 'blk':
+ tensor_group_name = f"{tensor_components[0]}.{tensor_components[1]}.{tensor_components[2]}"
+ elif tensor_components[0] in ['enc', 'dec']:
+ tensor_group_name = f"{tensor_components[0]}"
+
+ # Check if new Tensor Group
+ if tensor_group_name not in tensor_groups:
+ tensor_groups[tensor_group_name] = []
+ tensor_prefix_order.append(tensor_group_name)
+
+ # Record Tensor and Tensor Position
+ tensor_groups[tensor_group_name].append(tensor)
+ tensor_name_to_key[tensor.name] = key
+
+ # Tensors Mapping Dump
+ markdown_content += f'## Tensors Overview {element_count_rounded_notation(total_elements)} Elements\n\n'
+ markdown_content += f'Total number of elements in all tensors: {total_elements} Elements\n'
+ markdown_content += '\n'
+
+ for group in tensor_prefix_order:
+ tensors = tensor_groups[group]
+ group_elements = sum(tensor.n_elements for tensor in tensors)
+ markdown_content += f"- [{translate_tensor_name(group)} Tensor Group - {element_count_rounded_notation(group_elements)} Elements](#{group.replace('.', '_')})\n"
+
+ markdown_content += "\n"
+
+ markdown_content += "### Tensor Data Offset\n"
+ markdown_content += '\n'
+ markdown_content += 'This table contains the offset and data segment relative to start of file\n'
+ markdown_content += '\n'
+
+ tensor_mapping_table: list[dict[str, str | int]] = []
+ for key, tensor in enumerate(reader.tensors):
+ data_offset_pretty = '{0:#16x}'.format(tensor.data_offset)
+ data_size_pretty = '{0:#16x}'.format(tensor.n_bytes)
+ tensor_mapping_table.append({"t_id":key, "layer_name":tensor.name, "data_offset":data_offset_pretty, "data_size":data_size_pretty})
+
+ tensors_mapping_table_header_map = [
+ {'key_name':'t_id', 'header_name':'T_ID', 'align':'right'},
+ {'key_name':'layer_name', 'header_name':'Tensor Layer Name', 'align':'left'},
+ {'key_name':'data_offset', 'header_name':'Data Offset (B)', 'align':'right'},
+ {'key_name':'data_size', 'header_name':'Data Size (B)', 'align':'right'},
+ ]
+
+ markdown_content += markdown_table_with_alignment_support(tensors_mapping_table_header_map, tensor_mapping_table)
+ markdown_content += "\n"
+
+ for group in tensor_prefix_order:
+ tensors = tensor_groups[group]
+ group_elements = sum(tensor.n_elements for tensor in tensors)
+ group_percentage = group_elements / total_elements * 100
+ total_group_bytes = 0
+ total_group_elements = 0
+ markdown_content += f"### <a name=\"{group.replace('.', '_')}\">{translate_tensor_name(group)} Tensor Group : {element_count_rounded_notation(group_elements)} Elements</a>\n\n"
+
+ # Precalculate column sizing for visual consistency
+ prettify_element_est_count_size: int = 1
+ prettify_element_count_size: int = 1
+ prettify_dimension_max_widths: dict[int, int] = {}
+ for tensor in tensors:
+ prettify_element_est_count_size = max(prettify_element_est_count_size, len(str(element_count_rounded_notation(tensor.n_elements))))
+ prettify_element_count_size = max(prettify_element_count_size, len(str(tensor.n_elements)))
+ for i, dimension_size in enumerate(list(tensor.shape) + [1] * (4 - len(tensor.shape))):
+ prettify_dimension_max_widths[i] = max(prettify_dimension_max_widths.get(i,1), len(str(dimension_size)))
+
+ # Generate Tensor Layer Table Content
+ tensor_dump_table: list[dict[str, str | int]] = []
+ for tensor in tensors:
+ human_friendly_name = translate_tensor_name(tensor.name.replace(".weight", ".(W)").replace(".bias", ".(B)"))
+ pretty_dimension = ' x '.join(f'{str(d):>{prettify_dimension_max_widths[i]}}' for i, d in enumerate(list(tensor.shape) + [1] * (4 - len(tensor.shape))))
+ element_count_est = f"({element_count_rounded_notation(tensor.n_elements):>{prettify_element_est_count_size}})"
+ element_count_string = f"{element_count_est} {tensor.n_elements:>{prettify_element_count_size}}"
+ type_name_string = f"{tensor.tensor_type.name}"
+ if tensor.n_elements > 0:
+ bpw = (tensor.n_bytes * 8) / tensor.n_elements
+ else:
+ bpw = float('nan')
+ tensor_dump_table.append({"t_id":tensor_name_to_key[tensor.name], "layer_name":tensor.name, "human_layer_name":human_friendly_name, "element_count":element_count_string, "pretty_dimension":pretty_dimension, "tensor_type":type_name_string, "bpw": f"{bpw:.4f}"})
+ total_group_bytes += tensor.n_bytes
+ total_group_elements += tensor.n_elements
+
+ tensor_dump_table_header_map = [
+ {'key_name':'t_id', 'header_name':'T_ID', 'align':'right'},
+ {'key_name':'layer_name', 'header_name':'Tensor Layer Name', 'align':'left'},
+ {'key_name':'human_layer_name', 'header_name':'Human Friendly Tensor Layer Name', 'align':'left'},
+ {'key_name':'element_count', 'header_name':'Elements', 'align':'left'},
+ {'key_name':'pretty_dimension', 'header_name':'Shape', 'align':'left'},
+ {'key_name':'tensor_type', 'header_name':'Type', 'align':'left'},
+ {'key_name':'bpw', 'header_name':'BPW', 'align':'right'},
+ ]
+
+ markdown_content += markdown_table_with_alignment_support(tensor_dump_table_header_map, tensor_dump_table)
+
+ markdown_content += "\n"
+ markdown_content += f"- Total elements in {group}: ({element_count_rounded_notation(group_elements):>4}) {group_elements}\n"
+ markdown_content += f"- Percentage of total elements: {group_percentage:.2f}%\n"
+ if total_group_elements > 0:
+ total_group_bpw = (total_group_bytes * 8) / total_group_elements
+ markdown_content += f"- Bits per Weight (BPW) for {group}: {total_group_bpw:.4f} bits\n"
+ else:
+ markdown_content += f"- Bits per Weight (BPW) for {group}: undefined (no elements)\n"
+ markdown_content += "\n\n"
+ total_model_bytes += total_group_bytes
+ total_model_elements += total_group_elements
+
+ if total_model_elements > 0:
+ total_model_bpw = (total_model_bytes * 8) / total_model_elements
+ markdown_content += f"Total BPW for {os.path.basename(args.model)}: {total_model_bpw:.4f} bits"
+ else:
+ markdown_content += f"Total BPW for {os.path.basename(args.model)}: undefined (no elements)"
+ print(markdown_content) # noqa: NP100
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description="Dump GGUF file metadata")
+ parser.add_argument("model", type=str, help="GGUF format model filename")
+ parser.add_argument("--no-tensors", action="store_true", help="Don't dump tensor metadata")
+ parser.add_argument("--json", action="store_true", help="Produce JSON output")
+ parser.add_argument("--json-array", action="store_true", help="Include full array values in JSON output (long)")
+ parser.add_argument("--data-offset", action="store_true", help="Start of data offset")
+ parser.add_argument("--data-alignment", action="store_true", help="Data alignment applied globally to data field")
+ parser.add_argument("--markdown", action="store_true", help="Produce markdown output")
+ parser.add_argument("--verbose", action="store_true", help="increase output verbosity")
+
+ args = parser.parse_args(None if len(sys.argv) > 1 else ["--help"])
+
+ logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
+
+ if not args.json and not args.markdown and not args.data_offset and not args.data_alignment:
+ logger.info(f'* Loading: {args.model}')
+
+ reader = GGUFReader(args.model, 'r')
+
+ if args.json:
+ dump_metadata_json(reader, args)
+ elif args.markdown:
+ dump_markdown_metadata(reader, args)
+ elif args.data_offset:
+ print(reader.data_offset) # noqa: NP100
+ elif args.data_alignment:
+ print(reader.alignment) # noqa: NP100
+ else:
+ dump_metadata(reader, args)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/llama.cpp/gguf-py/gguf/scripts/gguf_editor_gui.py b/llama.cpp/gguf-py/gguf/scripts/gguf_editor_gui.py
new file mode 100755
index 0000000..293316a
--- /dev/null
+++ b/llama.cpp/gguf-py/gguf/scripts/gguf_editor_gui.py
@@ -0,0 +1,1621 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+import logging
+import argparse
+import os
+import sys
+import numpy
+import enum
+from pathlib import Path
+from typing import Any, Optional, Tuple, Type
+import warnings
+
+import numpy as np
+from PySide6.QtWidgets import (
+ QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
+ QPushButton, QLabel, QLineEdit, QFileDialog, QTableWidget,
+ QTableWidgetItem, QComboBox, QMessageBox, QTabWidget,
+ QTextEdit, QFormLayout,
+ QHeaderView, QDialog, QDialogButtonBox
+)
+from PySide6.QtCore import Qt
+
+# Necessary to load the local gguf package
+if "NO_LOCAL_GGUF" not in os.environ and (Path(__file__).parent.parent.parent.parent / 'gguf-py').exists():
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
+
+import gguf
+from gguf import GGUFReader, GGUFWriter, GGUFValueType, ReaderField
+from gguf.constants import TokenType, RopeScalingType, PoolingType, GGMLQuantizationType
+
+logger = logging.getLogger("gguf-editor-gui")
+
+# Map of key names to enum types for automatic enum interpretation
+KEY_TO_ENUM_TYPE = {
+ gguf.Keys.Tokenizer.TOKEN_TYPE: TokenType,
+ gguf.Keys.Rope.SCALING_TYPE: RopeScalingType,
+ gguf.Keys.LLM.POOLING_TYPE: PoolingType,
+ gguf.Keys.General.FILE_TYPE: GGMLQuantizationType,
+}
+
+# Define the tokenizer keys that should be edited together
+TOKENIZER_LINKED_KEYS = [
+ gguf.Keys.Tokenizer.LIST,
+ gguf.Keys.Tokenizer.TOKEN_TYPE,
+ gguf.Keys.Tokenizer.SCORES
+]
+
+
+class TokenizerEditorDialog(QDialog):
+ def __init__(self, tokens, token_types, scores, parent=None):
+ super().__init__(parent)
+ self.setWindowTitle("Edit Tokenizer Data")
+ self.resize(900, 600)
+
+ self.tokens = tokens.copy() if tokens else []
+ self.token_types = token_types.copy() if token_types else []
+ self.scores = scores.copy() if scores else []
+
+ # Ensure all arrays have the same length
+ max_len = max(len(self.tokens), len(self.token_types), len(self.scores))
+ if len(self.tokens) < max_len:
+ self.tokens.extend([""] * (max_len - len(self.tokens)))
+ if len(self.token_types) < max_len:
+ self.token_types.extend([0] * (max_len - len(self.token_types)))
+ if len(self.scores) < max_len:
+ self.scores.extend([0.0] * (max_len - len(self.scores)))
+
+ layout = QVBoxLayout(self)
+
+ # Add filter controls
+ filter_layout = QHBoxLayout()
+ filter_layout.addWidget(QLabel("Filter:"))
+ self.filter_edit = QLineEdit()
+ self.filter_edit.setPlaceholderText("Type to filter tokens...")
+ self.filter_edit.textChanged.connect(self.apply_filter)
+ filter_layout.addWidget(self.filter_edit)
+
+ # Add page controls
+ self.page_size = 100 # Show 100 items per page
+ self.current_page = 0
+ self.total_pages = max(1, (len(self.tokens) + self.page_size - 1) // self.page_size)
+
+ self.page_label = QLabel(f"Page 1 of {self.total_pages}")
+ filter_layout.addWidget(self.page_label)
+
+ prev_page = QPushButton("Previous")
+ prev_page.clicked.connect(self.previous_page)
+ filter_layout.addWidget(prev_page)
+
+ next_page = QPushButton("Next")
+ next_page.clicked.connect(self.next_page)
+ filter_layout.addWidget(next_page)
+
+ layout.addLayout(filter_layout)
+
+ # Tokenizer data table
+ self.tokens_table = QTableWidget()
+ self.tokens_table.setColumnCount(4)
+ self.tokens_table.setHorizontalHeaderLabels(["Index", "Token", "Type", "Score"])
+ self.tokens_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
+ self.tokens_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
+ self.tokens_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
+ self.tokens_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
+
+ layout.addWidget(self.tokens_table)
+
+ # Controls
+ controls_layout = QHBoxLayout()
+
+ add_button = QPushButton("Add Token")
+ add_button.clicked.connect(self.add_token)
+ controls_layout.addWidget(add_button)
+
+ remove_button = QPushButton("Remove Selected")
+ remove_button.clicked.connect(self.remove_selected)
+ controls_layout.addWidget(remove_button)
+
+ controls_layout.addStretch()
+
+ layout.addLayout(controls_layout)
+
+ # Buttons
+ buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
+ buttons.accepted.connect(self.accept)
+ buttons.rejected.connect(self.reject)
+ layout.addWidget(buttons)
+
+ # Initialize the filtered values
+ self.filtered_indices = list(range(len(self.tokens)))
+
+ # Load data for the first page
+ self.load_page()
+
+ def apply_filter(self):
+ """Filter the tokens based on the search text."""
+ filter_text = self.filter_edit.text().lower()
+
+ if not filter_text:
+ # No filter, show all values
+ self.filtered_indices = list(range(len(self.tokens)))
+ else:
+ # Apply filter
+ self.filtered_indices = []
+ for i, token in enumerate(self.tokens):
+ if filter_text in str(token).lower():
+ self.filtered_indices.append(i)
+
+ # Reset to first page and reload
+ self.total_pages = max(1, (len(self.filtered_indices) + self.page_size - 1) // self.page_size)
+ self.current_page = 0
+ self.page_label.setText(f"Page 1 of {self.total_pages}")
+ self.load_page()
+
+ def previous_page(self):
+ """Go to the previous page of results."""
+ if self.current_page > 0:
+ self.current_page -= 1
+ self.page_label.setText(f"Page {self.current_page + 1} of {self.total_pages}")
+ self.load_page()
+
+ def next_page(self):
+ """Go to the next page of results."""
+ if self.current_page < self.total_pages - 1:
+ self.current_page += 1
+ self.page_label.setText(f"Page {self.current_page + 1} of {self.total_pages}")
+ self.load_page()
+
+ def load_page(self):
+ """Load the current page of tokenizer data."""
+ self.tokens_table.setRowCount(0) # Clear the table
+
+ # Calculate start and end indices for the current page
+ start_idx = self.current_page * self.page_size
+ end_idx = min(start_idx + self.page_size, len(self.filtered_indices))
+
+ # Pre-allocate rows for better performance
+ self.tokens_table.setRowCount(end_idx - start_idx)
+
+ for row, i in enumerate(range(start_idx, end_idx)):
+ orig_idx = self.filtered_indices[i]
+
+ # Index
+ index_item = QTableWidgetItem(str(orig_idx))
+ index_item.setData(Qt.ItemDataRole.UserRole, orig_idx) # Store original index
+ index_item.setFlags(index_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
+ self.tokens_table.setItem(row, 0, index_item)
+
+ # Token
+ token_item = QTableWidgetItem(str(self.tokens[orig_idx]))
+ self.tokens_table.setItem(row, 1, token_item)
+
+ # Token Type
+ token_type = self.token_types[orig_idx] if orig_idx < len(self.token_types) else 0
+ try:
+ enum_val = TokenType(token_type)
+ display_text = f"{enum_val.name} ({token_type})"
+ except (ValueError, KeyError):
+ display_text = f"Unknown ({token_type})"
+
+ type_item = QTableWidgetItem(display_text)
+ type_item.setData(Qt.ItemDataRole.UserRole, token_type)
+
+ # Make type cell editable with a double-click handler
+ type_item.setFlags(type_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
+ self.tokens_table.setItem(row, 2, type_item)
+
+ # Score
+ score = self.scores[orig_idx] if orig_idx < len(self.scores) else 0.0
+ score_item = QTableWidgetItem(str(score))
+ self.tokens_table.setItem(row, 3, score_item)
+
+ # Connect double-click handler for token type cells
+ self.tokens_table.cellDoubleClicked.connect(self.handle_cell_double_click)
+
+ def handle_cell_double_click(self, row, column):
+ """Handle double-click on a cell, specifically for token type editing."""
+ if column == 2: # Token Type column
+ orig_item = self.tokens_table.item(row, 0)
+ if orig_item:
+ orig_idx = orig_item.data(Qt.ItemDataRole.UserRole)
+ self.edit_token_type(row, orig_idx)
+
+ def edit_token_type(self, row, orig_idx):
+ """Edit a token type using a dialog with a dropdown of all enum options."""
+ current_value = self.token_types[orig_idx] if orig_idx < len(self.token_types) else 0
+
+ # Create a dialog with enum options
+ dialog = QDialog(self)
+ dialog.setWindowTitle("Select Token Type")
+ layout = QVBoxLayout(dialog)
+
+ combo = QComboBox()
+ for enum_val in TokenType:
+ combo.addItem(f"{enum_val.name} ({enum_val.value})", enum_val.value)
+
+ # Set current value
+ try:
+ if isinstance(current_value, int):
+ enum_val = TokenType(current_value)
+ combo.setCurrentText(f"{enum_val.name} ({current_value})")
+ except (ValueError, KeyError):
+ pass
+
+ layout.addWidget(combo)
+
+ buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
+ buttons.accepted.connect(dialog.accept)
+ buttons.rejected.connect(dialog.reject)
+ layout.addWidget(buttons)
+
+ if dialog.exec() == QDialog.DialogCode.Accepted:
+ # Get the selected value
+ new_value = combo.currentData()
+ enum_val = TokenType(new_value)
+ display_text = f"{enum_val.name} ({new_value})"
+
+ # Update the display
+ type_item = self.tokens_table.item(row, 2)
+ if type_item:
+ type_item.setText(display_text)
+ type_item.setData(Qt.ItemDataRole.UserRole, new_value)
+
+ # Update the actual value
+ self.token_types[orig_idx] = new_value
+
+ def add_token(self):
+ """Add a new token to the end of the list."""
+ # Add to the end of the arrays
+ self.tokens.append("")
+ self.token_types.append(0) # Default to normal token
+ self.scores.append(0.0)
+
+ orig_idx = len(self.tokens) - 1
+
+ # Add to filtered indices if it matches the current filter
+ filter_text = self.filter_edit.text().lower()
+ if not filter_text or filter_text in "":
+ self.filtered_indices.append(orig_idx)
+
+ # Update pagination
+ self.total_pages = max(1, (len(self.filtered_indices) + self.page_size - 1) // self.page_size)
+
+ # Go to the last page to show the new item
+ self.current_page = self.total_pages - 1
+ self.page_label.setText(f"Page {self.current_page + 1} of {self.total_pages}")
+
+ # Reload the page
+ self.load_page()
+
+ def remove_selected(self):
+ """Remove selected tokens from all arrays."""
+ selected_rows = []
+ for item in self.tokens_table.selectedItems():
+ row = item.row()
+ if row not in selected_rows:
+ selected_rows.append(row)
+
+ if not selected_rows:
+ return
+
+ # Get original indices in descending order to avoid index shifting
+ orig_indices = []
+ for row in selected_rows:
+ orig_item = self.tokens_table.item(row, 0)
+ if orig_item:
+ orig_indices.append(orig_item.data(Qt.ItemDataRole.UserRole))
+ orig_indices.sort(reverse=True)
+
+ # Remove from all arrays
+ for idx in orig_indices:
+ if idx < len(self.tokens):
+ del self.tokens[idx]
+ if idx < len(self.token_types):
+ del self.token_types[idx]
+ if idx < len(self.scores):
+ del self.scores[idx]
+
+ # Rebuild filtered_indices
+ self.filtered_indices = []
+ filter_text = self.filter_edit.text().lower()
+
+ for i, token in enumerate(self.tokens):
+ if not filter_text or filter_text in str(token).lower():
+ self.filtered_indices.append(i)
+
+ # Update pagination
+ self.total_pages = max(1, (len(self.filtered_indices) + self.page_size - 1) // self.page_size)
+ self.current_page = min(self.current_page, self.total_pages - 1)
+ self.page_label.setText(f"Page {self.current_page + 1} of {self.total_pages}")
+
+ # Reload the page
+ self.load_page()
+
+ def get_data(self):
+ """Return the edited tokenizer data."""
+ return self.tokens, self.token_types, self.scores
+
+
+class ArrayEditorDialog(QDialog):
+ def __init__(self, array_values, element_type, key=None, parent=None):
+ super().__init__(parent)
+ self.setWindowTitle("Edit Array Values")
+ self.resize(700, 500)
+
+ self.array_values = array_values
+ self.element_type = element_type
+ self.key = key
+
+ # Get enum type for this array if applicable
+ self.enum_type = None
+ if key in KEY_TO_ENUM_TYPE and element_type == GGUFValueType.INT32:
+ self.enum_type = KEY_TO_ENUM_TYPE[key]
+
+ layout = QVBoxLayout(self)
+
+ # Add enum type information if applicable
+ if self.enum_type is not None:
+ enum_info_layout = QHBoxLayout()
+ enum_label = QLabel(f"Editing {self.enum_type.__name__} values:")
+ enum_info_layout.addWidget(enum_label)
+
+ # Add a legend for the enum values
+ enum_values = ", ".join([f"{e.name}={e.value}" for e in self.enum_type])
+ enum_values_label = QLabel(f"Available values: {enum_values}")
+ enum_values_label.setWordWrap(True)
+ enum_info_layout.addWidget(enum_values_label, 1)
+
+ layout.addLayout(enum_info_layout)
+
+ # Add search/filter controls
+ filter_layout = QHBoxLayout()
+ filter_layout.addWidget(QLabel("Filter:"))
+ self.filter_edit = QLineEdit()
+ self.filter_edit.setPlaceholderText("Type to filter values...")
+ self.filter_edit.textChanged.connect(self.apply_filter)
+ filter_layout.addWidget(self.filter_edit)
+
+ # Add page controls for large arrays
+ self.page_size = 100 # Show 100 items per page
+ self.current_page = 0
+ self.total_pages = max(1, (len(array_values) + self.page_size - 1) // self.page_size)
+
+ self.page_label = QLabel(f"Page 1 of {self.total_pages}")
+ filter_layout.addWidget(self.page_label)
+
+ prev_page = QPushButton("Previous")
+ prev_page.clicked.connect(self.previous_page)
+ filter_layout.addWidget(prev_page)
+
+ next_page = QPushButton("Next")
+ next_page.clicked.connect(self.next_page)
+ filter_layout.addWidget(next_page)
+
+ layout.addLayout(filter_layout)
+
+ # Array items table
+ self.items_table = QTableWidget()
+
+ # Set up columns based on whether we have an enum type
+ if self.enum_type is not None:
+ self.items_table.setColumnCount(3)
+ self.items_table.setHorizontalHeaderLabels(["Index", "Value", "Actions"])
+ self.items_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
+ self.items_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
+ self.items_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
+ else:
+ self.items_table.setColumnCount(2)
+ self.items_table.setHorizontalHeaderLabels(["Index", "Value"])
+ self.items_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
+ self.items_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
+
+ layout.addWidget(self.items_table)
+
+ # Controls
+ controls_layout = QHBoxLayout()
+
+ add_button = QPushButton("Add Item")
+ add_button.clicked.connect(self.add_item)
+ controls_layout.addWidget(add_button)
+
+ remove_button = QPushButton("Remove Selected")
+ remove_button.clicked.connect(self.remove_selected)
+ controls_layout.addWidget(remove_button)
+
+ # Add bulk edit button for enum arrays
+ if self.enum_type is not None:
+ bulk_edit_button = QPushButton("Bulk Edit Selected")
+ bulk_edit_button.clicked.connect(self.bulk_edit_selected)
+ controls_layout.addWidget(bulk_edit_button)
+
+ controls_layout.addStretch()
+
+ layout.addLayout(controls_layout)
+
+ # Buttons
+ buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
+ buttons.accepted.connect(self.accept)
+ buttons.rejected.connect(self.reject)
+ layout.addWidget(buttons)
+
+ # Initialize the filtered values
+ self.filtered_indices = list(range(len(self.array_values)))
+
+ # Load array values for the first page
+ self.load_page()
+
+ def apply_filter(self):
+ """Filter the array values based on the search text."""
+ filter_text = self.filter_edit.text().lower()
+
+ if not filter_text:
+ # No filter, show all values
+ self.filtered_indices = list(range(len(self.array_values)))
+ else:
+ # Apply filter
+ self.filtered_indices = []
+ for i, value in enumerate(self.array_values):
+ # For enum values, search in both name and value
+ if self.enum_type is not None and isinstance(value, int):
+ try:
+ enum_val = self.enum_type(value)
+ display_text = f"{enum_val.name} ({value})".lower()
+ if filter_text in display_text:
+ self.filtered_indices.append(i)
+ except (ValueError, KeyError):
+ # If not a valid enum value, just check the raw value
+ if filter_text in str(value).lower():
+ self.filtered_indices.append(i)
+ else:
+ # For non-enum values, just check the string representation
+ if filter_text in str(value).lower():
+ self.filtered_indices.append(i)
+
+ # Reset to first page and reload
+ self.total_pages = max(1, (len(self.filtered_indices) + self.page_size - 1) // self.page_size)
+ self.current_page = 0
+ self.page_label.setText(f"Page 1 of {self.total_pages}")
+ self.load_page()
+
+ def previous_page(self):
+ """Go to the previous page of results."""
+ if self.current_page > 0:
+ self.current_page -= 1
+ self.page_label.setText(f"Page {self.current_page + 1} of {self.total_pages}")
+ self.load_page()
+
+ def next_page(self):
+ """Go to the next page of results."""
+ if self.current_page < self.total_pages - 1:
+ self.current_page += 1
+ self.page_label.setText(f"Page {self.current_page + 1} of {self.total_pages}")
+ self.load_page()
+
+ def load_page(self):
+ """Load the current page of array values."""
+ self.items_table.setRowCount(0) # Clear the table
+
+ # Calculate start and end indices for the current page
+ start_idx = self.current_page * self.page_size
+ end_idx = min(start_idx + self.page_size, len(self.filtered_indices))
+
+ # Pre-allocate rows for better performance
+ self.items_table.setRowCount(end_idx - start_idx)
+
+ for row, i in enumerate(range(start_idx, end_idx)):
+ orig_idx = self.filtered_indices[i]
+ value = self.array_values[orig_idx]
+
+ # Index
+ index_item = QTableWidgetItem(str(orig_idx))
+ index_item.setData(Qt.ItemDataRole.UserRole, orig_idx) # Store original index
+ index_item.setFlags(index_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
+ self.items_table.setItem(row, 0, index_item)
+
+ # Value
+ if self.enum_type is not None:
+ # Display enum value and name
+ try:
+ if isinstance(value, (int, numpy.signedinteger)):
+ enum_val = self.enum_type(value)
+ display_text = f"{enum_val.name} ({value})"
+ else:
+ display_text = str(value)
+ except (ValueError, KeyError):
+ display_text = f"Unknown ({value})"
+
+ # Store the enum value in the item
+ value_item = QTableWidgetItem(display_text)
+ value_item.setData(Qt.ItemDataRole.UserRole, value)
+ value_item.setFlags(value_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
+ self.items_table.setItem(row, 1, value_item)
+
+ # Add an edit button in a separate column
+ edit_button = QPushButton("Edit")
+ edit_button.setProperty("row", row)
+ edit_button.clicked.connect(self.edit_array_enum_value)
+
+ # Create a widget to hold the button
+ button_widget = QWidget()
+ button_layout = QHBoxLayout(button_widget)
+ button_layout.setContentsMargins(2, 2, 2, 2)
+ button_layout.addWidget(edit_button)
+ button_layout.addStretch()
+
+ self.items_table.setCellWidget(row, 2, button_widget)
+ else:
+ value_item = QTableWidgetItem(str(value))
+ self.items_table.setItem(row, 1, value_item)
+
+ def edit_array_enum_value(self):
+ """Handle editing an enum value in the array editor."""
+ button = self.sender()
+ row = button.property("row")
+
+ # Get the original index from the table item
+ orig_item = self.items_table.item(row, 0)
+ new_item = self.items_table.item(row, 1)
+ if orig_item and new_item and self.enum_type and self.edit_enum_value(row, self.enum_type):
+ orig_idx = orig_item.data(Qt.ItemDataRole.UserRole)
+ new_value = new_item.data(Qt.ItemDataRole.UserRole)
+ # Update the stored value in the array
+ if isinstance(new_value, (int, float, str, bool)):
+ self.array_values[orig_idx] = new_value
+
+ def bulk_edit_selected(self):
+ """Edit multiple enum values at once."""
+ if not self.enum_type:
+ return
+
+ selected_rows = set()
+ for item in self.items_table.selectedItems():
+ selected_rows.add(item.row())
+
+ if not selected_rows:
+ QMessageBox.information(self, "No Selection", "Please select at least one row to edit.")
+ return
+
+ # Create a dialog with enum options
+ dialog = QDialog(self)
+ dialog.setWindowTitle(f"Bulk Edit {self.enum_type.__name__} Values")
+ layout = QVBoxLayout(dialog)
+
+ layout.addWidget(QLabel(f"Set {len(selected_rows)} selected items to:"))
+
+ combo = QComboBox()
+ for enum_val in self.enum_type:
+ combo.addItem(f"{enum_val.name} ({enum_val.value})", enum_val.value)
+
+ layout.addWidget(combo)
+
+ buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
+ buttons.accepted.connect(dialog.accept)
+ buttons.rejected.connect(dialog.reject)
+ layout.addWidget(buttons)
+
+ if dialog.exec() == QDialog.DialogCode.Accepted:
+ # Get the selected value
+ new_value = combo.currentData()
+ enum_val = self.enum_type(new_value)
+ display_text = f"{enum_val.name} ({new_value})"
+
+ # Update all selected rows
+ for row in selected_rows:
+ orig_item = self.items_table.item(row, 0)
+ new_item = self.items_table.item(row, 1)
+ if orig_item and new_item:
+ orig_idx = orig_item.data(Qt.ItemDataRole.UserRole)
+ self.array_values[orig_idx] = new_value
+
+ # Update the display
+ new_item.setText(display_text)
+ new_item.setData(Qt.ItemDataRole.UserRole, new_value)
+
+ def add_item(self):
+ # Add to the end of the array
+ orig_idx = len(self.array_values)
+
+ # Add default value based on type
+ if self.enum_type is not None:
+ # Default to first enum value
+ default_value = list(self.enum_type)[0].value
+ self.array_values.append(default_value)
+ else:
+ if self.element_type == GGUFValueType.STRING:
+ self.array_values.append("")
+ else:
+ self.array_values.append(0)
+
+ # Add to filtered indices if it matches the current filter
+ self.filtered_indices.append(orig_idx)
+
+ # Update pagination
+ self.total_pages = max(1, (len(self.filtered_indices) + self.page_size - 1) // self.page_size)
+
+ # Go to the last page to show the new item
+ self.current_page = self.total_pages - 1
+ self.page_label.setText(f"Page {self.current_page + 1} of {self.total_pages}")
+
+ # Reload the page
+ self.load_page()
+
+ def remove_selected(self):
+ selected_rows = []
+ for item in self.items_table.selectedItems():
+ row = item.row()
+ if row not in selected_rows:
+ selected_rows.append(row)
+
+ if not selected_rows:
+ return
+
+ # Get original indices in descending order to avoid index shifting
+ orig_indices = list()
+ for row in selected_rows:
+ orig_item = self.items_table.item(row, 0)
+ if orig_item:
+ orig_indices.append(orig_item.data(Qt.ItemDataRole.UserRole))
+ orig_indices.sort(reverse=True)
+
+ # Remove from array_values
+ for idx in orig_indices:
+ del self.array_values[idx]
+
+ # Rebuild filtered_indices
+ self.filtered_indices = []
+ filter_text = self.filter_edit.text().lower()
+
+ for i, value in enumerate(self.array_values):
+ if not filter_text:
+ self.filtered_indices.append(i)
+ else:
+ # Apply filter
+ if self.enum_type is not None and isinstance(value, int):
+ try:
+ enum_val = self.enum_type(value)
+ display_text = f"{enum_val.name} ({value})".lower()
+ if filter_text in display_text:
+ self.filtered_indices.append(i)
+ except (ValueError, KeyError):
+ if filter_text in str(value).lower():
+ self.filtered_indices.append(i)
+ else:
+ if filter_text in str(value).lower():
+ self.filtered_indices.append(i)
+
+ # Update pagination
+ self.total_pages = max(1, (len(self.filtered_indices) + self.page_size - 1) // self.page_size)
+ self.current_page = min(self.current_page, self.total_pages - 1)
+ self.page_label.setText(f"Page {self.current_page + 1} of {self.total_pages}")
+
+ # Reload the page
+ self.load_page()
+
+ def edit_enum_value(self, row: int, enum_type: Type[enum.Enum]):
+ """Edit an enum value using a dialog with a dropdown of all enum options."""
+ # Get the original index from the table item
+ orig_item = self.items_table.item(row, 0)
+ if orig_item:
+ orig_idx = orig_item.data(Qt.ItemDataRole.UserRole)
+ else:
+ return
+ current_value = self.array_values[orig_idx]
+
+ # Create a dialog with enum options
+ dialog = QDialog(self)
+ dialog.setWindowTitle(f"Select {enum_type.__name__} Value")
+ layout = QVBoxLayout(dialog)
+
+ # Add description
+ description = QLabel(f"Select a {enum_type.__name__} value:")
+ layout.addWidget(description)
+
+ # Use a combo box for quick selection
+ combo = QComboBox()
+ for enum_val in enum_type:
+ combo.addItem(f"{enum_val.name} ({enum_val.value})", enum_val.value)
+
+ # Set current value
+ try:
+ if isinstance(current_value, int):
+ enum_val = enum_type(current_value)
+ combo.setCurrentText(f"{enum_val.name} ({current_value})")
+ except (ValueError, KeyError):
+ pass
+
+ layout.addWidget(combo)
+
+ buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
+ buttons.accepted.connect(dialog.accept)
+ buttons.rejected.connect(dialog.reject)
+ layout.addWidget(buttons)
+
+ if dialog.exec() == QDialog.DialogCode.Accepted:
+ # Update the value display and stored data
+ new_value = combo.currentData()
+ enum_val = enum_type(new_value)
+ display_text = f"{enum_val.name} ({new_value})"
+
+ new_item = self.items_table.item(row, 1)
+ if new_item:
+ new_item.setText(display_text)
+ new_item.setData(Qt.ItemDataRole.UserRole, new_value)
+
+ # Update the actual array value
+ self.array_values[orig_idx] = new_value
+ return True
+ return False
+
+ def get_array_values(self):
+ # The array_values list is kept up-to-date as edits are made
+ return self.array_values
+
+
+class AddMetadataDialog(QDialog):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setWindowTitle("Add Metadata")
+ self.resize(400, 200)
+
+ layout = QVBoxLayout(self)
+
+ form_layout = QFormLayout()
+
+ self.key_edit = QLineEdit()
+ form_layout.addRow("Key:", self.key_edit)
+
+ self.type_combo = QComboBox()
+ for value_type in GGUFValueType:
+ if value_type != GGUFValueType.ARRAY: # Skip array type for simplicity
+ self.type_combo.addItem(value_type.name, value_type)
+ form_layout.addRow("Type:", self.type_combo)
+
+ self.value_edit = QTextEdit()
+ form_layout.addRow("Value:", self.value_edit)
+
+ layout.addLayout(form_layout)
+
+ buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
+ buttons.accepted.connect(self.accept)
+ buttons.rejected.connect(self.reject)
+ layout.addWidget(buttons)
+
+ def get_data(self) -> Tuple[str, GGUFValueType, Any]:
+ key = self.key_edit.text()
+ value_type = self.type_combo.currentData()
+ value_text = self.value_edit.toPlainText()
+
+ # Convert value based on type
+ if value_type == GGUFValueType.UINT8:
+ value = np.uint8(int(value_text))
+ elif value_type == GGUFValueType.INT8:
+ value = np.int8(int(value_text))
+ elif value_type == GGUFValueType.UINT16:
+ value = np.uint16(int(value_text))
+ elif value_type == GGUFValueType.INT16:
+ value = np.int16(int(value_text))
+ elif value_type == GGUFValueType.UINT32:
+ value = np.uint32(int(value_text))
+ elif value_type == GGUFValueType.INT32:
+ value = np.int32(int(value_text))
+ elif value_type == GGUFValueType.FLOAT32:
+ value = np.float32(float(value_text))
+ elif value_type == GGUFValueType.BOOL:
+ value = value_text.lower() in ('true', 'yes', '1')
+ elif value_type == GGUFValueType.STRING:
+ value = value_text
+ else:
+ value = value_text
+
+ return key, value_type, value
+
+
+class GGUFEditorWindow(QMainWindow):
+ def __init__(self):
+ super().__init__()
+
+ self.setWindowTitle("GGUF Editor")
+ self.resize(1000, 800)
+
+ self.current_file = None
+ self.reader = None
+ self.modified = False
+ self.metadata_changes = {} # Store changes to apply when saving
+ self.metadata_to_remove = set() # Store keys to remove when saving
+ self.on_metadata_changed_is_connected = False
+
+ self.setup_ui()
+
+ def setup_ui(self):
+ central_widget = QWidget()
+ self.setCentralWidget(central_widget)
+
+ main_layout = QVBoxLayout(central_widget)
+
+ # File controls
+ file_layout = QHBoxLayout()
+
+ self.file_path_edit = QLineEdit()
+ self.file_path_edit.setReadOnly(True)
+ file_layout.addWidget(self.file_path_edit)
+
+ open_button = QPushButton("Open GGUF")
+ open_button.clicked.connect(self.open_file)
+ file_layout.addWidget(open_button)
+
+ save_button = QPushButton("Save As...")
+ save_button.clicked.connect(self.save_file)
+ file_layout.addWidget(save_button)
+
+ main_layout.addLayout(file_layout)
+
+ # Tabs for different views
+ self.tabs = QTabWidget()
+
+ # Metadata tab
+ self.metadata_tab = QWidget()
+ metadata_layout = QVBoxLayout(self.metadata_tab)
+
+ # Metadata table
+ self.metadata_table = QTableWidget()
+ self.metadata_table.setColumnCount(4)
+ self.metadata_table.setHorizontalHeaderLabels(["Key", "Type", "Value", "Actions"])
+ self.metadata_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
+ self.metadata_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
+ self.metadata_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
+ self.metadata_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
+ metadata_layout.addWidget(self.metadata_table)
+
+ # Metadata controls
+ metadata_controls = QHBoxLayout()
+
+ add_metadata_button = QPushButton("Add Metadata")
+ add_metadata_button.clicked.connect(self.add_metadata)
+ metadata_controls.addWidget(add_metadata_button)
+
+ metadata_controls.addStretch()
+
+ metadata_layout.addLayout(metadata_controls)
+
+ # Tensors tab
+ self.tensors_tab = QWidget()
+ tensors_layout = QVBoxLayout(self.tensors_tab)
+
+ self.tensors_table = QTableWidget()
+ self.tensors_table.setColumnCount(5)
+ self.tensors_table.setHorizontalHeaderLabels(["Name", "Type", "Shape", "Elements", "Size (bytes)"])
+ self.tensors_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
+ self.tensors_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
+ self.tensors_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
+ self.tensors_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
+ self.tensors_table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.ResizeToContents)
+ tensors_layout.addWidget(self.tensors_table)
+
+ # Add tabs to tab widget
+ self.tabs.addTab(self.metadata_tab, "Metadata")
+ self.tabs.addTab(self.tensors_tab, "Tensors")
+
+ main_layout.addWidget(self.tabs)
+
+ # Status bar
+ self.statusBar().showMessage("Ready")
+
+ def load_file(self, file_path):
+ """Load a GGUF file by path"""
+ try:
+ self.statusBar().showMessage(f"Loading {file_path}...")
+ QApplication.processEvents()
+
+ self.reader = GGUFReader(file_path, 'r')
+ self.current_file = file_path
+ self.file_path_edit.setText(file_path)
+
+ self.load_metadata()
+ self.load_tensors()
+
+ self.metadata_changes = {}
+ self.metadata_to_remove = set()
+ self.modified = False
+
+ self.statusBar().showMessage(f"Loaded {file_path}")
+ return True
+ except Exception as e:
+ QMessageBox.critical(self, "Error", f"Failed to open file: {str(e)}")
+ self.statusBar().showMessage("Error loading file")
+ return False
+
+ def open_file(self):
+ file_path, _ = QFileDialog.getOpenFileName(
+ self, "Open GGUF File", "", "GGUF Files (*.gguf);;All Files (*)"
+ )
+
+ if not file_path:
+ return
+
+ self.load_file(file_path)
+
+ def load_metadata(self):
+ self.metadata_table.setRowCount(0)
+
+ if not self.reader:
+ return
+
+ # Disconnect to prevent triggering during loading
+ if self.on_metadata_changed_is_connected:
+ with warnings.catch_warnings():
+ warnings.filterwarnings('ignore')
+ self.metadata_table.itemChanged.disconnect(self.on_metadata_changed)
+ self.on_metadata_changed_is_connected = False
+
+ for i, (key, field) in enumerate(self.reader.fields.items()):
+ self.metadata_table.insertRow(i)
+
+ # Key
+ key_item = QTableWidgetItem(key)
+ key_item.setFlags(key_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
+ self.metadata_table.setItem(i, 0, key_item)
+
+ # Type
+ if not field.types:
+ type_str = "N/A"
+ elif field.types[0] == GGUFValueType.ARRAY:
+ nest_count = len(field.types) - 1
+ element_type = field.types[-1].name
+ # Check if this is an enum array
+ enum_type = self.get_enum_for_key(key)
+ if enum_type is not None and field.types[-1] == GGUFValueType.INT32:
+ element_type = enum_type.__name__
+ type_str = '[' * nest_count + element_type + ']' * nest_count
+ else:
+ type_str = str(field.types[0].name)
+ # Check if this is an enum field
+ enum_type = self.get_enum_for_key(key)
+ if enum_type is not None and field.types[0] == GGUFValueType.INT32:
+ type_str = enum_type.__name__
+
+ type_item = QTableWidgetItem(type_str)
+ type_item.setFlags(type_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
+ self.metadata_table.setItem(i, 1, type_item)
+
+ # Value
+ value_str = self.format_field_value(field)
+ value_item = QTableWidgetItem(value_str)
+
+ # Make only simple values editable
+ if len(field.types) == 1 and field.types[0] != GGUFValueType.ARRAY:
+ value_item.setFlags(value_item.flags() | Qt.ItemFlag.ItemIsEditable)
+ else:
+ value_item.setFlags(value_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
+
+ self.metadata_table.setItem(i, 2, value_item)
+
+ # Actions
+ actions_widget = QWidget()
+ actions_layout = QHBoxLayout(actions_widget)
+ actions_layout.setContentsMargins(2, 2, 2, 2)
+
+ # Add Edit button for arrays and enum fields
+ if field.types and field.types[0] == GGUFValueType.ARRAY:
+ edit_button = QPushButton("Edit")
+ edit_button.setProperty("row", i)
+ edit_button.setProperty("key", key)
+ edit_button.clicked.connect(self.edit_array_metadata)
+ actions_layout.addWidget(edit_button)
+
+ # Add special label for tokenizer linked fields
+ if key in TOKENIZER_LINKED_KEYS:
+ edit_button.setText("Edit Tokenizer")
+ edit_button.setToolTip("Edit all tokenizer data together")
+ elif len(field.types) == 1 and self.get_enum_for_key(key) is not None:
+ edit_button = QPushButton("Edit")
+ edit_button.setProperty("row", i)
+ edit_button.setProperty("key", key)
+ edit_button.clicked.connect(self.edit_metadata_enum)
+ actions_layout.addWidget(edit_button)
+
+ remove_button = QPushButton("Remove")
+ remove_button.setProperty("row", i)
+ remove_button.setProperty("key", key)
+ remove_button.clicked.connect(self.remove_metadata)
+ actions_layout.addWidget(remove_button)
+
+ self.metadata_table.setCellWidget(i, 3, actions_widget)
+
+ # Reconnect after loading
+ self.metadata_table.itemChanged.connect(self.on_metadata_changed)
+ self.on_metadata_changed_is_connected = True
+
+ def extract_array_values(self, field: ReaderField) -> list:
+ """Extract all values from an array field."""
+ if not field.types or field.types[0] != GGUFValueType.ARRAY:
+ return []
+
+ curr_type = field.types[1]
+ array_values = []
+ total_elements = len(field.data)
+
+ if curr_type == GGUFValueType.STRING:
+ for element_pos in range(total_elements):
+ value_string = str(bytes(field.parts[-1 - (total_elements - element_pos - 1) * 2]), encoding='utf-8')
+ array_values.append(value_string)
+ elif self.reader and curr_type in self.reader.gguf_scalar_to_np:
+ for element_pos in range(total_elements):
+ array_values.append(field.parts[-1 - (total_elements - element_pos - 1)][0])
+
+ return array_values
+
+ def get_enum_for_key(self, key: str) -> Optional[Type[enum.Enum]]:
+ """Get the enum type for a given key if it exists."""
+ return KEY_TO_ENUM_TYPE.get(key)
+
+ def format_enum_value(self, value: Any, enum_type: Type[enum.Enum]) -> str:
+ """Format a value as an enum if possible."""
+ try:
+ if isinstance(value, (int, str)):
+ enum_value = enum_type(value)
+ return f"{enum_value.name} ({value})"
+ except (ValueError, KeyError):
+ pass
+ return str(value)
+
+ def format_field_value(self, field: ReaderField) -> str:
+ if not field.types:
+ return "N/A"
+
+ if len(field.types) == 1:
+ curr_type = field.types[0]
+ if curr_type == GGUFValueType.STRING:
+ return str(bytes(field.parts[-1]), encoding='utf-8')
+ elif self.reader and curr_type in self.reader.gguf_scalar_to_np:
+ value = field.parts[-1][0]
+ # Check if this field has an enum type
+ enum_type = self.get_enum_for_key(field.name)
+ if enum_type is not None:
+ return self.format_enum_value(value, enum_type)
+ return str(value)
+
+ if field.types[0] == GGUFValueType.ARRAY:
+ array_values = self.extract_array_values(field)
+ render_element = min(5, len(array_values))
+
+ # Get enum type for this array if applicable
+ enum_type = self.get_enum_for_key(field.name)
+
+ if enum_type is not None:
+ array_elements = []
+ for i in range(render_element):
+ array_elements.append(self.format_enum_value(array_values[i], enum_type))
+ else:
+ array_elements = [str(array_values[i]) for i in range(render_element)]
+
+ return f"[ {', '.join(array_elements).strip()}{', ...' if len(array_values) > len(array_elements) else ''} ]"
+
+ return "Complex value"
+
+ def load_tensors(self):
+ self.tensors_table.setRowCount(0)
+
+ if not self.reader:
+ return
+
+ for i, tensor in enumerate(self.reader.tensors):
+ self.tensors_table.insertRow(i)
+
+ # Name
+ name_item = QTableWidgetItem(tensor.name)
+ name_item.setFlags(name_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
+ self.tensors_table.setItem(i, 0, name_item)
+
+ # Type
+ type_item = QTableWidgetItem(tensor.tensor_type.name)
+ type_item.setFlags(type_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
+ self.tensors_table.setItem(i, 1, type_item)
+
+ # Shape
+ shape_str = " × ".join(str(d) for d in tensor.shape)
+ shape_item = QTableWidgetItem(shape_str)
+ shape_item.setFlags(shape_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
+ self.tensors_table.setItem(i, 2, shape_item)
+
+ # Elements
+ elements_item = QTableWidgetItem(str(tensor.n_elements))
+ elements_item.setFlags(elements_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
+ self.tensors_table.setItem(i, 3, elements_item)
+
+ # Size
+ size_item = QTableWidgetItem(f"{tensor.n_bytes:,}")
+ size_item.setFlags(size_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
+ self.tensors_table.setItem(i, 4, size_item)
+
+ def on_metadata_changed(self, item):
+ if item.column() != 2: # Only handle value column changes
+ return
+
+ row = item.row()
+ orig_item = self.metadata_table.item(row, 0)
+ key = None
+ if orig_item:
+ key = orig_item.text()
+ new_value = item.text()
+
+ field = None
+ if self.reader and key:
+ field = self.reader.get_field(key)
+ if not field or not field.types or not key:
+ return
+
+ value_type = field.types[0]
+
+ # Check if this is an enum field
+ enum_type = self.get_enum_for_key(key)
+ if enum_type is not None and value_type == GGUFValueType.INT32:
+ # Try to parse the enum value from the text
+ try:
+ # Check if it's a name
+ try:
+ enum_val = enum_type[new_value]
+ converted_value = enum_val.value
+ except (KeyError, AttributeError):
+ # Check if it's a number or "NAME (value)" format
+ if '(' in new_value and ')' in new_value:
+ # Extract the value from "NAME (value)" format
+ value_part = new_value.split('(')[1].split(')')[0].strip()
+ converted_value = int(value_part)
+ else:
+ # Try to convert directly to int
+ converted_value = int(new_value)
+
+ # Validate that it's a valid enum value
+ enum_type(converted_value)
+
+ # Store the change
+ self.metadata_changes[key] = (value_type, converted_value)
+ self.modified = True
+
+ # Update display with formatted enum value
+ formatted_value = self.format_enum_value(converted_value, enum_type)
+ item.setText(formatted_value)
+
+ self.statusBar().showMessage(f"Changed {key} to {formatted_value}")
+ return
+ except (ValueError, KeyError) as e:
+ QMessageBox.warning(
+ self,
+ f"Invalid Enum Value ({e})",
+ f"'{new_value}' is not a valid {enum_type.__name__} value.\n"
+ f"Valid values are: {', '.join(v.name for v in enum_type)}")
+
+ # Revert to original value
+ original_value = self.format_field_value(field)
+ item.setText(original_value)
+ return
+
+ try:
+ # Convert the string value to the appropriate type
+ if value_type == GGUFValueType.UINT8:
+ converted_value = np.uint8(int(new_value))
+ elif value_type == GGUFValueType.INT8:
+ converted_value = np.int8(int(new_value))
+ elif value_type == GGUFValueType.UINT16:
+ converted_value = np.uint16(int(new_value))
+ elif value_type == GGUFValueType.INT16:
+ converted_value = np.int16(int(new_value))
+ elif value_type == GGUFValueType.UINT32:
+ converted_value = np.uint32(int(new_value))
+ elif value_type == GGUFValueType.INT32:
+ converted_value = np.int32(int(new_value))
+ elif value_type == GGUFValueType.FLOAT32:
+ converted_value = np.float32(float(new_value))
+ elif value_type == GGUFValueType.BOOL:
+ converted_value = new_value.lower() in ('true', 'yes', '1')
+ elif value_type == GGUFValueType.STRING:
+ converted_value = new_value
+ else:
+ # Unsupported type for editing
+ return
+
+ # Store the change
+ self.metadata_changes[key] = (value_type, converted_value)
+ self.modified = True
+
+ self.statusBar().showMessage(f"Changed {key} to {new_value}")
+ except ValueError:
+ QMessageBox.warning(self, "Invalid Value", f"The value '{new_value}' is not valid for type {value_type.name}")
+
+ # Revert to original value
+ original_value = self.format_field_value(field)
+ item.setText(original_value)
+
+ def remove_metadata(self):
+ button = self.sender()
+ key = button.property("key")
+ row = button.property("row")
+
+ reply = QMessageBox.question(
+ self, "Confirm Removal",
+ f"Are you sure you want to remove the metadata key '{key}'?",
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No
+ )
+
+ if reply == QMessageBox.StandardButton.Yes:
+ self.metadata_table.removeRow(row)
+ self.metadata_to_remove.add(key)
+
+ # If we previously had changes for this key, remove them
+ if key in self.metadata_changes:
+ del self.metadata_changes[key]
+
+ self.modified = True
+ self.statusBar().showMessage(f"Marked {key} for removal")
+
+ def edit_metadata_enum(self):
+ """Edit an enum metadata field."""
+ button = self.sender()
+ key = button.property("key")
+ row = button.property("row")
+
+ field = None
+ if self.reader:
+ field = self.reader.get_field(key)
+ if not field or not field.types:
+ return
+
+ enum_type = self.get_enum_for_key(key)
+ if enum_type is None:
+ return
+
+ # Get current value
+ current_value = field.contents()
+
+ # Create a dialog with enum options
+ dialog = QDialog(self)
+ dialog.setWindowTitle(f"Select {enum_type.__name__} Value")
+ layout = QVBoxLayout(dialog)
+
+ combo = QComboBox()
+ for enum_val in enum_type:
+ combo.addItem(f"{enum_val.name} ({enum_val.value})", enum_val.value)
+
+ # Set current value
+ try:
+ if isinstance(current_value, (int, str)):
+ enum_val = enum_type(current_value)
+ combo.setCurrentText(f"{enum_val.name} ({current_value})")
+ except (ValueError, KeyError):
+ pass
+
+ layout.addWidget(combo)
+
+ buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
+ buttons.accepted.connect(dialog.accept)
+ buttons.rejected.connect(dialog.reject)
+ layout.addWidget(buttons)
+
+ if dialog.exec() == QDialog.DialogCode.Accepted:
+ # Get the selected value
+ new_value = combo.currentData()
+ enum_val = enum_type(new_value)
+
+ # Store the change
+ self.metadata_changes[key] = (field.types[0], new_value)
+ self.modified = True
+
+ # Update display
+ display_text = f"{enum_val.name} ({new_value})"
+ target_item = self.metadata_table.item(row, 2)
+ if target_item:
+ target_item.setText(display_text)
+
+ self.statusBar().showMessage(f"Changed {key} to {display_text}")
+
+ def edit_array_metadata(self):
+ button = self.sender()
+ key = button.property("key")
+ row = button.property("row")
+
+ # Check if this is one of the linked tokenizer keys
+ if key in TOKENIZER_LINKED_KEYS:
+ self.edit_tokenizer_metadata(key)
+ return
+
+ field = None
+ if self.reader:
+ field = self.reader.get_field(key)
+ if not field or not field.types or field.types[0] != GGUFValueType.ARRAY:
+ return
+
+ # Get array element type
+ element_type = field.types[1]
+
+ # Extract array values
+ array_values = self.extract_array_values(field)
+
+ # Open array editor dialog
+ dialog = ArrayEditorDialog(array_values, element_type, key, self)
+ if dialog.exec() == QDialog.DialogCode.Accepted:
+ new_values = dialog.get_array_values()
+
+ # Store the change
+ self.metadata_changes[key] = (GGUFValueType.ARRAY, (element_type, new_values))
+ self.modified = True
+
+ # Update display
+ enum_type = self.get_enum_for_key(key)
+ if enum_type is not None and element_type == GGUFValueType.INT32:
+ value_str = f"[ {', '.join(self.format_enum_value(v, enum_type) for v in new_values[:5])}{', ...' if len(new_values) > 5 else ''} ]"
+ else:
+ value_str = f"[ {', '.join(str(v) for v in new_values[:5])}{', ...' if len(new_values) > 5 else ''} ]"
+ target_item = self.metadata_table.item(row, 2)
+ if target_item:
+ target_item.setText(value_str)
+
+ self.statusBar().showMessage(f"Updated array values for {key}")
+
+ def edit_tokenizer_metadata(self, trigger_key):
+ """Edit the linked tokenizer metadata arrays together."""
+ if not self.reader:
+ return
+
+ # Get all three fields
+ tokens_field = self.reader.get_field(gguf.Keys.Tokenizer.LIST)
+ token_types_field = self.reader.get_field(gguf.Keys.Tokenizer.TOKEN_TYPE)
+ scores_field = self.reader.get_field(gguf.Keys.Tokenizer.SCORES)
+
+ # Extract values from each field
+ tokens = self.extract_array_values(tokens_field) if tokens_field else []
+ token_types = self.extract_array_values(token_types_field) if token_types_field else []
+ scores = self.extract_array_values(scores_field) if scores_field else []
+
+ # Apply any pending changes
+ if gguf.Keys.Tokenizer.LIST in self.metadata_changes:
+ _, (_, tokens) = self.metadata_changes[gguf.Keys.Tokenizer.LIST]
+ if gguf.Keys.Tokenizer.TOKEN_TYPE in self.metadata_changes:
+ _, (_, token_types) = self.metadata_changes[gguf.Keys.Tokenizer.TOKEN_TYPE]
+ if gguf.Keys.Tokenizer.SCORES in self.metadata_changes:
+ _, (_, scores) = self.metadata_changes[gguf.Keys.Tokenizer.SCORES]
+
+ # Open the tokenizer editor dialog
+ dialog = TokenizerEditorDialog(tokens, token_types, scores, self)
+ if dialog.exec() == QDialog.DialogCode.Accepted:
+ new_tokens, new_token_types, new_scores = dialog.get_data()
+
+ # Store changes for all three arrays
+ if tokens_field:
+ self.metadata_changes[gguf.Keys.Tokenizer.LIST] = (
+ GGUFValueType.ARRAY,
+ (tokens_field.types[1], new_tokens)
+ )
+
+ if token_types_field:
+ self.metadata_changes[gguf.Keys.Tokenizer.TOKEN_TYPE] = (
+ GGUFValueType.ARRAY,
+ (token_types_field.types[1], new_token_types)
+ )
+
+ if scores_field:
+ self.metadata_changes[gguf.Keys.Tokenizer.SCORES] = (
+ GGUFValueType.ARRAY,
+ (scores_field.types[1], new_scores)
+ )
+
+ self.modified = True
+
+ # Update display for all three fields
+ self.update_tokenizer_display(gguf.Keys.Tokenizer.LIST, new_tokens)
+ self.update_tokenizer_display(gguf.Keys.Tokenizer.TOKEN_TYPE, new_token_types)
+ self.update_tokenizer_display(gguf.Keys.Tokenizer.SCORES, new_scores)
+
+ self.statusBar().showMessage("Updated tokenizer data")
+
+ def update_tokenizer_display(self, key, values):
+ """Update the display of a tokenizer field in the metadata table."""
+ for row in range(self.metadata_table.rowCount()):
+ key_item = self.metadata_table.item(row, 0)
+ if key_item and key_item.text() == key:
+ value_str = f"[ {', '.join(str(v) for v in values[:5])}{', ...' if len(values) > 5 else ''} ]"
+ value_item = self.metadata_table.item(row, 2)
+ if value_item:
+ value_item.setText(value_str)
+ break
+
+ def add_metadata(self):
+ dialog = AddMetadataDialog(self)
+ if dialog.exec() == QDialog.DialogCode.Accepted:
+ key, value_type, value = dialog.get_data()
+
+ if not key:
+ QMessageBox.warning(self, "Invalid Key", "Key cannot be empty")
+ return
+
+ # Check if key already exists
+ for row in range(self.metadata_table.rowCount()):
+ orig_item = self.metadata_table.item(row, 0)
+ if orig_item and orig_item.text() == key:
+ QMessageBox.warning(self, "Duplicate Key", f"Key '{key}' already exists")
+ return
+
+ # Add to table
+ row = self.metadata_table.rowCount()
+ self.metadata_table.insertRow(row)
+
+ # Key
+ key_item = QTableWidgetItem(key)
+ key_item.setFlags(key_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
+ self.metadata_table.setItem(row, 0, key_item)
+
+ # Type
+ type_item = QTableWidgetItem(value_type.name)
+ type_item.setFlags(type_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
+ self.metadata_table.setItem(row, 1, type_item)
+
+ # Value
+ value_item = QTableWidgetItem(str(value))
+ value_item.setFlags(value_item.flags() | Qt.ItemFlag.ItemIsEditable)
+ self.metadata_table.setItem(row, 2, value_item)
+
+ # Actions
+ actions_widget = QWidget()
+ actions_layout = QHBoxLayout(actions_widget)
+ actions_layout.setContentsMargins(2, 2, 2, 2)
+
+ remove_button = QPushButton("Remove")
+ remove_button.setProperty("row", row)
+ remove_button.setProperty("key", key)
+ remove_button.clicked.connect(self.remove_metadata)
+ actions_layout.addWidget(remove_button)
+
+ self.metadata_table.setCellWidget(row, 3, actions_widget)
+
+ # Store the change
+ self.metadata_changes[key] = (value_type, value)
+ self.modified = True
+
+ self.statusBar().showMessage(f"Added new metadata key {key}")
+
+ def save_file(self):
+ if not self.reader:
+ QMessageBox.warning(self, "No File Open", "Please open a GGUF file first")
+ return
+
+ if not self.modified and not self.metadata_changes and not self.metadata_to_remove:
+ QMessageBox.information(self, "No Changes", "No changes to save")
+ return
+
+ file_path, _ = QFileDialog.getSaveFileName(
+ self, "Save GGUF File As", "", "GGUF Files (*.gguf);;All Files (*)"
+ )
+
+ if not file_path:
+ return
+
+ try:
+ self.statusBar().showMessage(f"Saving to {file_path}...")
+ QApplication.processEvents()
+
+ # Get architecture and endianness from the original file
+ arch = 'unknown'
+ field = self.reader.get_field(gguf.Keys.General.ARCHITECTURE)
+ if field:
+ arch = field.contents()
+
+ # Create writer
+ writer = GGUFWriter(file_path, arch=arch, endianess=self.reader.endianess)
+
+ # Get alignment if present
+ alignment = None
+ field = self.reader.get_field(gguf.Keys.General.ALIGNMENT)
+ if field:
+ alignment = field.contents()
+ if alignment is not None:
+ writer.data_alignment = alignment
+
+ # Copy metadata with changes
+ for field in self.reader.fields.values():
+ # Skip virtual fields and fields written by GGUFWriter
+ if field.name == gguf.Keys.General.ARCHITECTURE or field.name.startswith('GGUF.'):
+ continue
+
+ # Skip fields marked for removal
+ if field.name in self.metadata_to_remove:
+ continue
+
+ # Apply changes if any
+ sub_type = None
+ if field.name in self.metadata_changes:
+ value_type, value = self.metadata_changes[field.name]
+ if value_type == GGUFValueType.ARRAY:
+ # Handle array values
+ sub_type, value = value
+ else:
+ # Copy original value
+ value = field.contents()
+ value_type = field.types[0]
+ if value_type == GGUFValueType.ARRAY:
+ sub_type = field.types[-1]
+
+ if value is not None:
+ writer.add_key_value(field.name, value, value_type, sub_type=sub_type)
+
+ # Add new metadata
+ for key, (value_type, value) in self.metadata_changes.items():
+ # Skip if the key already existed (we handled it above)
+ if self.reader.get_field(key) is not None:
+ continue
+
+ sub_type = None
+ if value_type == GGUFValueType.ARRAY:
+ # Handle array values
+ sub_type, value = value
+
+ writer.add_key_value(key, value, value_type, sub_type=sub_type)
+
+ # Add tensors (including data)
+ for tensor in self.reader.tensors:
+ writer.add_tensor(tensor.name, tensor.data, raw_shape=tensor.data.shape, raw_dtype=tensor.tensor_type, tensor_endianess=self.reader.endianess)
+
+ # Write header and metadata
+ writer.open_output_file(Path(file_path))
+ writer.write_header_to_file()
+ writer.write_kv_data_to_file()
+
+ # Write tensor data using the optimized method
+ writer.write_tensors_to_file(progress=False)
+
+ writer.close()
+
+ self.statusBar().showMessage(f"Saved to {file_path}")
+
+ # Ask if user wants to open the new file
+ reply = QMessageBox.question(
+ self, "Open Saved File",
+ "Would you like to open the newly saved file?",
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.Yes
+ )
+
+ if reply == QMessageBox.StandardButton.Yes:
+ self.reader = GGUFReader(file_path, 'r')
+ self.current_file = file_path
+ self.file_path_edit.setText(file_path)
+
+ self.load_metadata()
+ self.load_tensors()
+
+ self.metadata_changes = {}
+ self.metadata_to_remove = set()
+ self.modified = False
+
+ except Exception as e:
+ QMessageBox.critical(self, "Error", f"Failed to save file: {str(e)}")
+ self.statusBar().showMessage("Error saving file")
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description="GUI GGUF Editor")
+ parser.add_argument("model_path", nargs="?", help="path to GGUF model file to load at startup")
+ parser.add_argument("--verbose", action="store_true", help="increase output verbosity")
+
+ args = parser.parse_args()
+
+ logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
+
+ app = QApplication(sys.argv)
+ window = GGUFEditorWindow()
+ window.show()
+
+ # Load model if specified
+ if args.model_path:
+ if os.path.isfile(args.model_path) and args.model_path.endswith('.gguf'):
+ window.load_file(args.model_path)
+ else:
+ logger.error(f"Invalid model path: {args.model_path}")
+ QMessageBox.warning(
+ window,
+ "Invalid Model Path",
+ f"The specified file does not exist or is not a GGUF file: {args.model_path}")
+
+ sys.exit(app.exec())
+
+
+if __name__ == '__main__':
+ main()
diff --git a/llama.cpp/gguf-py/gguf/scripts/gguf_hash.py b/llama.cpp/gguf-py/gguf/scripts/gguf_hash.py
new file mode 100755
index 0000000..3ef9899
--- /dev/null
+++ b/llama.cpp/gguf-py/gguf/scripts/gguf_hash.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+import uuid
+import hashlib
+
+import logging
+import argparse
+import os
+import sys
+from pathlib import Path
+
+from tqdm import tqdm
+
+# Necessary to load the local gguf package
+if "NO_LOCAL_GGUF" not in os.environ and (Path(__file__).parent.parent.parent.parent / 'gguf-py').exists():
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
+
+from gguf import GGUFReader # noqa: E402
+
+
+logger = logging.getLogger("gguf-hash")
+
+# UUID_NAMESPACE_LLAMA_CPP = uuid.uuid5(uuid.NAMESPACE_URL, 'en.wikipedia.org/wiki/Llama.cpp')
+UUID_NAMESPACE_LLAMA_CPP = uuid.UUID('ef001206-dadc-5f6d-a15f-3359e577d4e5')
+
+
+# For more information about what field.parts and field.data represent,
+# please see the comments in the modify_gguf.py example.
+def gguf_hash(reader: GGUFReader, filename: str, disable_progress_bar: bool, no_layer: bool) -> None:
+ sha1 = hashlib.sha1()
+ sha256 = hashlib.sha256()
+ uuidv5_sha1 = hashlib.sha1()
+ uuidv5_sha1.update(UUID_NAMESPACE_LLAMA_CPP.bytes)
+
+ # Total Weight Calculation For Progress Bar
+ total_weights = 0
+ for n, tensor in enumerate(reader.tensors, 1):
+
+ # We don't need these
+ if tensor.name.endswith((".attention.masked_bias", ".attention.bias", ".rotary_emb.inv_freq")):
+ continue
+
+ # Calculate Tensor Volume
+ sum_weights_in_tensor = 1
+ for dim in tensor.shape:
+ sum_weights_in_tensor *= dim
+ total_weights += sum_weights_in_tensor
+
+ # Hash Progress Bar
+ bar = tqdm(desc="Hashing", total=total_weights, unit="weights", unit_scale=True, disable=disable_progress_bar)
+
+ # Hashing Process
+ for tensor in reader.tensors:
+
+ # We don't need these
+ if tensor.name.endswith((".attention.masked_bias", ".attention.bias", ".rotary_emb.inv_freq")):
+ continue
+
+ # Progressbar
+ sum_weights_in_tensor = 1
+ for dim in tensor.shape:
+ sum_weights_in_tensor *= dim
+ bar.update(sum_weights_in_tensor)
+
+ if not no_layer:
+
+ sha1_layer = hashlib.sha1()
+ sha1_layer.update(tensor.data.data)
+ print("sha1 {0} {1}:{2}".format(sha1_layer.hexdigest(), filename, tensor.name)) # noqa: NP100
+
+ sha256_layer = hashlib.sha256()
+ sha256_layer.update(tensor.data.data)
+ print("sha256 {0} {1}:{2}".format(sha256_layer.hexdigest(), filename, tensor.name)) # noqa: NP100
+
+ sha1.update(tensor.data.data)
+ sha256.update(tensor.data.data)
+ uuidv5_sha1.update(tensor.data.data)
+
+ # Flush Hash Progress Bar
+ bar.close()
+
+ # Display Hash Output
+ print("sha1 {0} {1}".format(sha1.hexdigest(), filename)) # noqa: NP100
+ print("sha256 {0} {1}".format(sha256.hexdigest(), filename)) # noqa: NP100
+ print("uuid {0} {1}".format(uuid.UUID(bytes=uuidv5_sha1.digest()[:16], version=5), filename)) # noqa: NP100
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description="Dump GGUF file metadata")
+ parser.add_argument("model", type=str, help="GGUF format model filename")
+ parser.add_argument("--no-layer", action="store_true", help="exclude per layer hash")
+ parser.add_argument("--verbose", action="store_true", help="increase output verbosity")
+ parser.add_argument("--progressbar", action="store_true", help="enable progressbar")
+ args = parser.parse_args(None if len(sys.argv) > 1 else ["--help"])
+ logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
+ reader = GGUFReader(args.model, 'r')
+ gguf_hash(reader, args.model, not args.progressbar, args.no_layer)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/llama.cpp/gguf-py/gguf/scripts/gguf_new_metadata.py b/llama.cpp/gguf-py/gguf/scripts/gguf_new_metadata.py
new file mode 100755
index 0000000..c67436b
--- /dev/null
+++ b/llama.cpp/gguf-py/gguf/scripts/gguf_new_metadata.py
@@ -0,0 +1,216 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+import logging
+import argparse
+import os
+import sys
+import json
+from pathlib import Path
+
+from tqdm import tqdm
+from typing import Any, Sequence, NamedTuple
+
+# Necessary to load the local gguf package
+if "NO_LOCAL_GGUF" not in os.environ and (Path(__file__).parent.parent.parent.parent / 'gguf-py').exists():
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
+
+import gguf
+
+logger = logging.getLogger("gguf-new-metadata")
+
+
+class MetadataDetails(NamedTuple):
+ type: gguf.GGUFValueType
+ value: Any
+ description: str = ''
+ sub_type: gguf.GGUFValueType | None = None
+
+
+def get_field_data(reader: gguf.GGUFReader, key: str) -> Any:
+ field = reader.get_field(key)
+
+ return field.contents() if field else None
+
+
+def find_token(token_list: Sequence[int], token: str) -> Sequence[int]:
+ token_ids = [index for index, value in enumerate(token_list) if value == token]
+
+ if len(token_ids) == 0:
+ raise LookupError(f'Unable to find "{token}" in token list!')
+
+ return token_ids
+
+
+def copy_with_new_metadata(reader: gguf.GGUFReader, writer: gguf.GGUFWriter, new_metadata: dict[str, MetadataDetails], remove_metadata: Sequence[str]) -> None:
+ for field in reader.fields.values():
+ # Suppress virtual fields and fields written by GGUFWriter
+ if field.name == gguf.Keys.General.ARCHITECTURE or field.name.startswith('GGUF.'):
+ logger.debug(f'Suppressing {field.name}')
+ continue
+
+ # Skip old chat templates if we have new ones
+ if field.name.startswith(gguf.Keys.Tokenizer.CHAT_TEMPLATE) and gguf.Keys.Tokenizer.CHAT_TEMPLATE in new_metadata:
+ logger.debug(f'Skipping {field.name}')
+ continue
+
+ if field.name in remove_metadata:
+ logger.debug(f'Removing {field.name}')
+ continue
+
+ val_type = field.types[0]
+ sub_type = field.types[-1] if val_type == gguf.GGUFValueType.ARRAY else None
+ old_val = MetadataDetails(val_type, field.contents(), sub_type=sub_type)
+ val = new_metadata.get(field.name, old_val)
+
+ if field.name in new_metadata:
+ logger.debug(f'Modifying {field.name}: "{old_val.value}" -> "{val.value}" {val.description}')
+ del new_metadata[field.name]
+ elif val.value is not None:
+ logger.debug(f'Copying {field.name}')
+
+ if val.value is not None:
+ writer.add_key_value(field.name, val.value, val.type, sub_type=sub_type if val.sub_type is None else val.sub_type)
+
+ if gguf.Keys.Tokenizer.CHAT_TEMPLATE in new_metadata:
+ logger.debug('Adding chat template(s)')
+ writer.add_chat_template(new_metadata[gguf.Keys.Tokenizer.CHAT_TEMPLATE].value)
+ del new_metadata[gguf.Keys.Tokenizer.CHAT_TEMPLATE]
+
+ for key, val in new_metadata.items():
+ logger.debug(f'Adding {key}: "{val.value}" {val.description}')
+ writer.add_key_value(key, val.value, val.type)
+
+ total_bytes = 0
+
+ for tensor in reader.tensors:
+ total_bytes += tensor.n_bytes
+ writer.add_tensor_info(tensor.name, tensor.data.shape, tensor.data.dtype, tensor.data.nbytes, tensor.tensor_type)
+
+ bar = tqdm(desc="Writing", total=total_bytes, unit="byte", unit_scale=True)
+
+ writer.write_header_to_file()
+ writer.write_kv_data_to_file()
+ writer.write_ti_data_to_file()
+
+ for tensor in reader.tensors:
+ writer.write_tensor_data(tensor.data, tensor_endianess=reader.endianess)
+ bar.update(tensor.n_bytes)
+
+ writer.close()
+
+
+def main() -> None:
+ tokenizer_metadata = (getattr(gguf.Keys.Tokenizer, n) for n in gguf.Keys.Tokenizer.__dict__.keys() if not n.startswith('_'))
+ token_names = dict((n.split('.')[-1][:-len('_token_id')], n) for n in tokenizer_metadata if n.endswith('_token_id'))
+
+ parser = argparse.ArgumentParser(description="Make a copy of a GGUF file with new metadata")
+ parser.add_argument("input", type=Path, help="GGUF format model input filename")
+ parser.add_argument("output", type=Path, help="GGUF format model output filename")
+ parser.add_argument("--general-name", type=str, help="The models general.name", metavar='"name"')
+ parser.add_argument("--general-description", type=str, help="The models general.description", metavar='"Description ..."')
+ parser.add_argument("--chat-template", type=str, help="Chat template string (or JSON string containing templates)", metavar='"{% ... %} ..."')
+ parser.add_argument("--chat-template-config", type=Path, help="Config file containing chat template(s)", metavar='tokenizer_config.json')
+ parser.add_argument("--chat-template-file", type=Path, help="Jinja file containing chat template", metavar='chat_template.jinja')
+ parser.add_argument("--pre-tokenizer", type=str, help="The models tokenizer.ggml.pre", metavar='"pre tokenizer"')
+ parser.add_argument("--remove-metadata", action="append", type=str, help="Remove metadata (by key name) from output model", metavar='general.url')
+ parser.add_argument("--special-token", action="append", type=str, help="Special token by value", nargs=2, metavar=(' | '.join(token_names.keys()), '"<token>"'))
+ parser.add_argument("--special-token-by-id", action="append", type=str, help="Special token by id", nargs=2, metavar=(' | '.join(token_names.keys()), '0'))
+ parser.add_argument("--force", action="store_true", help="Bypass warnings without confirmation")
+ parser.add_argument("--verbose", action="store_true", help="Increase output verbosity")
+ args = parser.parse_args(None if len(sys.argv) > 2 else ["--help"])
+
+ logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
+
+ new_metadata = {}
+ remove_metadata = args.remove_metadata or []
+
+ if args.general_name:
+ new_metadata[gguf.Keys.General.NAME] = MetadataDetails(gguf.GGUFValueType.STRING, args.general_name)
+
+ if args.general_description:
+ new_metadata[gguf.Keys.General.DESCRIPTION] = MetadataDetails(gguf.GGUFValueType.STRING, args.general_description)
+
+ if args.chat_template:
+ new_metadata[gguf.Keys.Tokenizer.CHAT_TEMPLATE] = MetadataDetails(gguf.GGUFValueType.STRING, json.loads(args.chat_template) if args.chat_template.startswith('[') else args.chat_template)
+
+ if args.chat_template_config:
+ with open(args.chat_template_config, 'r', encoding='utf-8') as fp:
+ config = json.load(fp)
+ template = config.get('chat_template')
+ if template:
+ new_metadata[gguf.Keys.Tokenizer.CHAT_TEMPLATE] = MetadataDetails(gguf.GGUFValueType.STRING, template)
+
+ if args.chat_template_file:
+ with open(args.chat_template_file, 'r', encoding='utf-8') as fp:
+ template = fp.read()
+ new_metadata[gguf.Keys.Tokenizer.CHAT_TEMPLATE] = MetadataDetails(gguf.GGUFValueType.STRING, template)
+
+ if args.pre_tokenizer:
+ new_metadata[gguf.Keys.Tokenizer.PRE] = MetadataDetails(gguf.GGUFValueType.STRING, args.pre_tokenizer)
+
+ if remove_metadata:
+ logger.warning('*** Warning *** Warning *** Warning **')
+ logger.warning('* Most metadata is required for a fully functional GGUF file,')
+ logger.warning('* removing crucial metadata may result in a corrupt output file!')
+
+ if not args.force:
+ logger.warning('* Enter exactly YES if you are positive you want to proceed:')
+ response = input('YES, I am sure> ')
+ if response != 'YES':
+ logger.info("You didn't enter YES. Okay then, see ya!")
+ sys.exit(0)
+
+ logger.info(f'* Loading: {args.input}')
+ reader = gguf.GGUFReader(args.input, 'r')
+
+ arch = get_field_data(reader, gguf.Keys.General.ARCHITECTURE)
+
+ token_list = get_field_data(reader, gguf.Keys.Tokenizer.LIST) or []
+
+ for name, token in args.special_token or []:
+ if name not in token_names:
+ logger.warning(f'Unknown special token "{name}", ignoring...')
+ else:
+ ids = find_token(token_list, token)
+ new_metadata[token_names[name]] = MetadataDetails(gguf.GGUFValueType.UINT32, ids[0], f'= {token}')
+
+ if len(ids) > 1:
+ logger.warning(f'Multiple "{token}" tokens found, choosing ID {ids[0]}, use --special-token-by-id if you want another:')
+ logger.warning(', '.join(str(i) for i in ids))
+
+ for name, id_string in args.special_token_by_id or []:
+ if name not in token_names:
+ logger.warning(f'Unknown special token "{name}", ignoring...')
+ elif not id_string.isdecimal():
+ raise LookupError(f'Token ID "{id_string}" is not a valid ID!')
+ else:
+ id_int = int(id_string)
+
+ if id_int >= 0 and id_int < len(token_list):
+ new_metadata[token_names[name]] = MetadataDetails(gguf.GGUFValueType.UINT32, id_int, f'= {token_list[id_int]}')
+ else:
+ raise LookupError(f'Token ID {id_int} is not within token list!')
+
+ if os.path.isfile(args.output) and not args.force:
+ logger.warning('*** Warning *** Warning *** Warning **')
+ logger.warning(f'* The "{args.output}" GGUF file already exists, it will be overwritten!')
+ logger.warning('* Enter exactly YES if you are positive you want to proceed:')
+ response = input('YES, I am sure> ')
+ if response != 'YES':
+ logger.info("You didn't enter YES. Okay then, see ya!")
+ sys.exit(0)
+
+ logger.info(f'* Writing: {args.output}')
+ writer = gguf.GGUFWriter(args.output, arch=arch, endianess=reader.endianess)
+
+ alignment = get_field_data(reader, gguf.Keys.General.ALIGNMENT)
+ if alignment is not None:
+ logger.debug(f'Setting custom alignment: {alignment}')
+ writer.data_alignment = alignment
+
+ copy_with_new_metadata(reader, writer, new_metadata, remove_metadata)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/llama.cpp/gguf-py/gguf/scripts/gguf_set_metadata.py b/llama.cpp/gguf-py/gguf/scripts/gguf_set_metadata.py
new file mode 100755
index 0000000..f5809c3
--- /dev/null
+++ b/llama.cpp/gguf-py/gguf/scripts/gguf_set_metadata.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python3
+import logging
+import argparse
+import os
+import sys
+from pathlib import Path
+
+# Necessary to load the local gguf package
+if "NO_LOCAL_GGUF" not in os.environ and (Path(__file__).parent.parent.parent.parent / 'gguf-py').exists():
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
+
+from gguf import GGUFReader # noqa: E402
+
+logger = logging.getLogger("gguf-set-metadata")
+
+
+def minimal_example(filename: str) -> None:
+ reader = GGUFReader(filename, 'r+')
+ field = reader.fields['tokenizer.ggml.bos_token_id']
+ if field is None:
+ return
+ part_index = field.data[0]
+ field.parts[part_index][0] = 2 # Set tokenizer.ggml.bos_token_id to 2
+ #
+ # So what's this field.data thing? It's helpful because field.parts contains
+ # _every_ part of the GGUF field. For example, tokenizer.ggml.bos_token_id consists
+ # of:
+ #
+ # Part index 0: Key length (27)
+ # Part index 1: Key data ("tokenizer.ggml.bos_token_id")
+ # Part index 2: Field type (4, the id for GGUFValueType.UINT32)
+ # Part index 3: Field value
+ #
+ # Note also that each part is an NDArray slice, so even a part that
+ # is only a single value like the key length will be a NDArray of
+ # the key length type (numpy.uint32).
+ #
+ # The .data attribute in the Field is a list of relevant part indexes
+ # and doesn't contain internal GGUF details like the key length part.
+ # In this case, .data will be [3] - just the part index of the
+ # field value itself.
+
+
+def set_metadata(reader: GGUFReader, args: argparse.Namespace) -> None:
+ field = reader.get_field(args.key)
+ if field is None:
+ logger.error(f'! Field {repr(args.key)} not found')
+ sys.exit(1)
+ # Note that field.types is a list of types. This is because the GGUF
+ # format supports arrays. For example, an array of UINT32 would
+ # look like [GGUFValueType.ARRAY, GGUFValueType.UINT32]
+ handler = reader.gguf_scalar_to_np.get(field.types[0]) if field.types else None
+ if handler is None:
+ logger.error(f'! This tool only supports changing simple values, {repr(args.key)} has unsupported type {field.types}')
+ sys.exit(1)
+ current_value = field.parts[field.data[0]][0]
+ new_value = handler(args.value)
+ logger.info(f'* Preparing to change field {repr(args.key)} from {current_value} to {new_value}')
+ if current_value == new_value:
+ logger.info(f'- Key {repr(args.key)} already set to requested value {current_value}')
+ sys.exit(0)
+ if args.dry_run:
+ sys.exit(0)
+ if not args.force:
+ logger.warning('*** Warning *** Warning *** Warning **')
+ logger.warning('* Changing fields in a GGUF file can make it unusable. Proceed at your own risk.')
+ logger.warning('* Enter exactly YES if you are positive you want to proceed:')
+ response = input('YES, I am sure> ')
+ if response != 'YES':
+ logger.info("You didn't enter YES. Okay then, see ya!")
+ sys.exit(0)
+ field.parts[field.data[0]][0] = new_value
+ logger.info('* Field changed. Successful completion.')
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description="Set a simple value in GGUF file metadata")
+ parser.add_argument("model", type=str, help="GGUF format model filename")
+ parser.add_argument("key", type=str, help="Metadata key to set")
+ parser.add_argument("value", type=str, help="Metadata value to set")
+ parser.add_argument("--dry-run", action="store_true", help="Don't actually change anything")
+ parser.add_argument("--force", action="store_true", help="Change the field without confirmation")
+ parser.add_argument("--verbose", action="store_true", help="increase output verbosity")
+
+ args = parser.parse_args(None if len(sys.argv) > 1 else ["--help"])
+
+ logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
+
+ logger.info(f'* Loading: {args.model}')
+ reader = GGUFReader(args.model, 'r' if args.dry_run else 'r+')
+ set_metadata(reader, args)
+
+
+if __name__ == '__main__':
+ main()