|
14 | 14 | if "NO_LOCAL_GGUF" not in os.environ and (Path(__file__).parent.parent.parent / 'gguf-py').exists():
|
15 | 15 | sys.path.insert(0, str(Path(__file__).parent.parent))
|
16 | 16 |
|
17 |
| -from gguf import GGUFReader, GGUFValueType # noqa: E402 |
| 17 | +from gguf import GGUFReader, GGUFValueType, ReaderTensor # noqa: E402 |
18 | 18 |
|
19 | 19 | logger = logging.getLogger("gguf-dump")
|
20 | 20 |
|
@@ -101,25 +101,285 @@ def dump_metadata_json(reader: GGUFReader, args: argparse.Namespace) -> None:
|
101 | 101 | json.dump(result, sys.stdout)
|
102 | 102 |
|
103 | 103 |
|
| 104 | +def markdown_table_with_alignment_support(header_map: list[dict[str, str]], data: list[dict[str, Any]]): |
| 105 | + # JSON to Markdown table formatting: https://stackoverflow.com/a/72983854/2850957 |
| 106 | + |
| 107 | + # Alignment Utility Function |
| 108 | + def strAlign(padding: int, alignMode: str | None, strVal: str): |
| 109 | + if alignMode == 'center': |
| 110 | + return strVal.center(padding) |
| 111 | + elif alignMode == 'right': |
| 112 | + return strVal.rjust(padding - 1) + ' ' |
| 113 | + elif alignMode == 'left': |
| 114 | + return ' ' + strVal.ljust(padding - 1) |
| 115 | + else: # default left |
| 116 | + return ' ' + strVal.ljust(padding - 1) |
| 117 | + |
| 118 | + def dashAlign(padding: int, alignMode: str | None): |
| 119 | + if alignMode == 'center': |
| 120 | + return ':' + '-' * (padding - 2) + ':' |
| 121 | + elif alignMode == 'right': |
| 122 | + return '-' * (padding - 1) + ':' |
| 123 | + elif alignMode == 'left': |
| 124 | + return ':' + '-' * (padding - 1) |
| 125 | + else: # default left |
| 126 | + return '-' * (padding) |
| 127 | + |
| 128 | + # Calculate Padding For Each Column Based On Header and Data Length |
| 129 | + rowsPadding = {} |
| 130 | + for index, columnEntry in enumerate(header_map): |
| 131 | + padCount = max([len(str(v)) for d in data for k, v in d.items() if k == columnEntry['key_name']], default=0) + 2 |
| 132 | + headerPadCount = len(columnEntry['header_name']) + 2 |
| 133 | + rowsPadding[index] = headerPadCount if padCount <= headerPadCount else padCount |
| 134 | + |
| 135 | + # Render Markdown Header |
| 136 | + rows = [] |
| 137 | + rows.append('|'.join(strAlign(rowsPadding[index], columnEntry.get('align'), str(columnEntry['header_name'])) for index, columnEntry in enumerate(header_map))) |
| 138 | + rows.append('|'.join(dashAlign(rowsPadding[index], columnEntry.get('align')) for index, columnEntry in enumerate(header_map))) |
| 139 | + |
| 140 | + # Render Tabular Data |
| 141 | + for item in data: |
| 142 | + rows.append('|'.join(strAlign(rowsPadding[index], columnEntry.get('align'), str(item[columnEntry['key_name']])) for index, columnEntry in enumerate(header_map))) |
| 143 | + |
| 144 | + # Convert Tabular String Rows Into String |
| 145 | + tableString = "" |
| 146 | + for row in rows: |
| 147 | + tableString += f'|{row}|\n' |
| 148 | + |
| 149 | + return tableString |
| 150 | + |
| 151 | + |
| 152 | +def element_count_rounded_notation(count: int) -> str: |
| 153 | + if count > 1e15 : |
| 154 | + # Quadrillion |
| 155 | + scaled_amount = count * 1e-15 |
| 156 | + scale_suffix = "Q" |
| 157 | + elif count > 1e12 : |
| 158 | + # Trillions |
| 159 | + scaled_amount = count * 1e-12 |
| 160 | + scale_suffix = "T" |
| 161 | + elif count > 1e9 : |
| 162 | + # Billions |
| 163 | + scaled_amount = count * 1e-9 |
| 164 | + scale_suffix = "B" |
| 165 | + elif count > 1e6 : |
| 166 | + # Millions |
| 167 | + scaled_amount = count * 1e-6 |
| 168 | + scale_suffix = "M" |
| 169 | + elif count > 1e3 : |
| 170 | + # Thousands |
| 171 | + scaled_amount = count * 1e-3 |
| 172 | + scale_suffix = "K" |
| 173 | + else: |
| 174 | + # Under Thousands |
| 175 | + scaled_amount = count |
| 176 | + scale_suffix = "" |
| 177 | + return f"{'~' if count > 1e3 else ''}{round(scaled_amount)}{scale_suffix}" |
| 178 | + |
| 179 | + |
| 180 | +def translate_tensor_name(name): |
| 181 | + words = name.split(".") |
| 182 | + |
| 183 | + # Source: https://github.com/ggerganov/ggml/blob/master/docs/gguf.md#standardized-tensor-names |
| 184 | + abbreviation_dictionary = { |
| 185 | + 'token_embd': 'Token embedding', |
| 186 | + 'pos_embd': 'Position embedding', |
| 187 | + 'output_norm': 'Output normalization', |
| 188 | + 'output': 'Output', |
| 189 | + 'attn_norm': 'Attention normalization', |
| 190 | + 'attn_norm_2': 'Attention normalization', |
| 191 | + 'attn_qkv': 'Attention query-key-value', |
| 192 | + 'attn_q': 'Attention query', |
| 193 | + 'attn_k': 'Attention key', |
| 194 | + 'attn_v': 'Attention value', |
| 195 | + 'attn_output': 'Attention output', |
| 196 | + 'ffn_norm': 'Feed-forward network normalization', |
| 197 | + 'ffn_up': 'Feed-forward network "up"', |
| 198 | + 'ffn_gate': 'Feed-forward network "gate"', |
| 199 | + 'ffn_down': 'Feed-forward network "down"', |
| 200 | + 'ffn_gate_inp': 'Expert-routing layer for the Feed-forward network in Mixture of Expert models', |
| 201 | + 'ffn_gate_exp': 'Feed-forward network "gate" layer per expert in Mixture of Expert models', |
| 202 | + 'ffn_down_exp': 'Feed-forward network "down" layer per expert in Mixture of Expert models', |
| 203 | + 'ffn_up_exp': 'Feed-forward network "up" layer per expert in Mixture of Expert models', |
| 204 | + 'ssm_in': 'State space model input projections', |
| 205 | + 'ssm_conv1d': 'State space model rolling/shift', |
| 206 | + 'ssm_x': 'State space model selective parametrization', |
| 207 | + 'ssm_a': 'State space model state compression', |
| 208 | + 'ssm_d': 'State space model skip connection', |
| 209 | + 'ssm_dt': 'State space model time step', |
| 210 | + 'ssm_out': 'State space model output projection', |
| 211 | + 'blk': 'Block' |
| 212 | + } |
| 213 | + |
| 214 | + expanded_words = [] |
| 215 | + for word in words: |
| 216 | + word_norm = word.strip().lower() |
| 217 | + if word_norm in abbreviation_dictionary: |
| 218 | + expanded_words.append(abbreviation_dictionary[word_norm].title()) |
| 219 | + else: |
| 220 | + expanded_words.append(word.title()) |
| 221 | + |
| 222 | + return ' '.join(expanded_words) |
| 223 | + |
| 224 | + |
| 225 | +def dump_markdown_metadata(reader: GGUFReader, args: argparse.Namespace) -> None: |
| 226 | + host_endian, file_endian = get_file_host_endian(reader) |
| 227 | + markdown_content = "" |
| 228 | + markdown_content += f'# {args.model} - GGUF Internal File Dump\n\n' |
| 229 | + markdown_content += f'- Endian: {file_endian} endian\n' |
| 230 | + markdown_content += '\n' |
| 231 | + markdown_content += '## Key Value Metadata Store\n\n' |
| 232 | + markdown_content += f'There are {len(reader.fields)} key-value pairs in this file\n' |
| 233 | + markdown_content += '\n' |
| 234 | + |
| 235 | + kv_dump_table: list[dict[str, str | int]] = [] |
| 236 | + for n, field in enumerate(reader.fields.values(), 1): |
| 237 | + if not field.types: |
| 238 | + pretty_type = 'N/A' |
| 239 | + elif field.types[0] == GGUFValueType.ARRAY: |
| 240 | + nest_count = len(field.types) - 1 |
| 241 | + pretty_type = '[' * nest_count + str(field.types[-1].name) + ']' * nest_count |
| 242 | + else: |
| 243 | + pretty_type = str(field.types[-1].name) |
| 244 | + |
| 245 | + total_elements = len(field.data) |
| 246 | + value = "" |
| 247 | + if len(field.types) == 1: |
| 248 | + curr_type = field.types[0] |
| 249 | + if curr_type == GGUFValueType.STRING: |
| 250 | + value = repr(str(bytes(field.parts[-1]), encoding='utf-8')[:60]) |
| 251 | + elif curr_type in reader.gguf_scalar_to_np: |
| 252 | + value = str(field.parts[-1][0]) |
| 253 | + else: |
| 254 | + if field.types[0] == GGUFValueType.ARRAY: |
| 255 | + curr_type = field.types[1] |
| 256 | + if curr_type == GGUFValueType.STRING: |
| 257 | + render_element = min(5, total_elements) |
| 258 | + for element_pos in range(render_element): |
| 259 | + value += repr(str(bytes(field.parts[-1 - element_pos]), encoding='utf-8')[:5]) + (", " if total_elements > 1 else "") |
| 260 | + elif curr_type in reader.gguf_scalar_to_np: |
| 261 | + render_element = min(7, total_elements) |
| 262 | + for element_pos in range(render_element): |
| 263 | + value += str(field.parts[-1 - element_pos][0]) + (", " if total_elements > 1 else "") |
| 264 | + value = f'[ {value}{" ..." if total_elements > 1 else ""} ]' |
| 265 | + kv_dump_table.append({"n":n, "pretty_type":pretty_type, "total_elements":total_elements, "field_name":field.name, "value":value}) |
| 266 | + |
| 267 | + kv_dump_table_header_map = [ |
| 268 | + {'key_name':'n', 'header_name':'POS', 'align':'right'}, |
| 269 | + {'key_name':'pretty_type', 'header_name':'TYPE', 'align':'left'}, |
| 270 | + {'key_name':'total_elements', 'header_name':'Count', 'align':'right'}, |
| 271 | + {'key_name':'field_name', 'header_name':'Key', 'align':'left'}, |
| 272 | + {'key_name':'value', 'header_name':'Value', 'align':'left'}, |
| 273 | + ] |
| 274 | + |
| 275 | + markdown_content += markdown_table_with_alignment_support(kv_dump_table_header_map, kv_dump_table) |
| 276 | + |
| 277 | + markdown_content += "\n" |
| 278 | + |
| 279 | + if not args.no_tensors: |
| 280 | + # Group tensors by their prefix and maintain order |
| 281 | + tensor_prefix_order: list[str] = [] |
| 282 | + tensor_name_to_key: dict[str, int] = {} |
| 283 | + tensor_groups: dict[str, list[ReaderTensor]] = {} |
| 284 | + total_elements = sum(tensor.n_elements for tensor in reader.tensors) |
| 285 | + |
| 286 | + # Parsing Tensors Record |
| 287 | + for key, tensor in enumerate(reader.tensors): |
| 288 | + tensor_components = tensor.name.split('.') |
| 289 | + |
| 290 | + # Classify Tensor Group |
| 291 | + tensor_group_name = "base" |
| 292 | + if tensor_components[0] == 'blk': |
| 293 | + tensor_group_name = f"{tensor_components[0]}.{tensor_components[1]}" |
| 294 | + |
| 295 | + # Check if new Tensor Group |
| 296 | + if tensor_group_name not in tensor_groups: |
| 297 | + tensor_groups[tensor_group_name] = [] |
| 298 | + tensor_prefix_order.append(tensor_group_name) |
| 299 | + |
| 300 | + # Record Tensor and Tensor Position |
| 301 | + tensor_groups[tensor_group_name].append(tensor) |
| 302 | + tensor_name_to_key[tensor.name] = key |
| 303 | + |
| 304 | + # Tensors Mapping Dump |
| 305 | + markdown_content += f'## Tensors Overview {element_count_rounded_notation(total_elements)} Elements\n\n' |
| 306 | + markdown_content += f'Total number of elements in all tensors: {total_elements} Elements\n' |
| 307 | + markdown_content += '\n' |
| 308 | + |
| 309 | + for group in tensor_prefix_order: |
| 310 | + tensors = tensor_groups[group] |
| 311 | + group_elements = sum(tensor.n_elements for tensor in tensors) |
| 312 | + markdown_content += f"- [{translate_tensor_name(group)} Tensor Group - {element_count_rounded_notation(group_elements)} Elements](#{group.replace('.', '_')})\n" |
| 313 | + |
| 314 | + markdown_content += "\n" |
| 315 | + |
| 316 | + for group in tensor_prefix_order: |
| 317 | + tensors = tensor_groups[group] |
| 318 | + group_elements = sum(tensor.n_elements for tensor in tensors) |
| 319 | + group_percentage = group_elements / total_elements * 100 |
| 320 | + markdown_content += f"### <a name=\"{group.replace('.', '_')}\">{translate_tensor_name(group)} Tensor Group : {element_count_rounded_notation(group_elements)} Elements</a>\n\n" |
| 321 | + |
| 322 | + # Precalculate column sizing for visual consistency |
| 323 | + prettify_element_est_count_size: int = 1 |
| 324 | + prettify_element_count_size: int = 1 |
| 325 | + prettify_dimension_max_widths: dict[int, int] = {} |
| 326 | + for tensor in tensors: |
| 327 | + prettify_element_est_count_size = max(prettify_element_est_count_size, len(str(element_count_rounded_notation(tensor.n_elements)))) |
| 328 | + prettify_element_count_size = max(prettify_element_count_size, len(str(tensor.n_elements))) |
| 329 | + for i, dimension_size in enumerate(list(tensor.shape) + [1] * (4 - len(tensor.shape))): |
| 330 | + prettify_dimension_max_widths[i] = max(prettify_dimension_max_widths.get(i,1), len(str(dimension_size))) |
| 331 | + |
| 332 | + # Generate Tensor Layer Table Content |
| 333 | + tensor_dump_table: list[dict[str, str | int]] = [] |
| 334 | + for tensor in tensors: |
| 335 | + human_friendly_name = translate_tensor_name(tensor.name.replace(".weight", ".(W)").replace(".bias", ".(B)")) |
| 336 | + 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)))) |
| 337 | + element_count_est = f"({element_count_rounded_notation(tensor.n_elements):>{prettify_element_est_count_size}})" |
| 338 | + element_count_string = f"{element_count_est} {tensor.n_elements:>{prettify_element_count_size}}" |
| 339 | + type_name_string = f"{tensor.tensor_type.name}" |
| 340 | + 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}) |
| 341 | + |
| 342 | + tensor_dump_table_header_map = [ |
| 343 | + {'key_name':'t_id', 'header_name':'T_ID', 'align':'right'}, |
| 344 | + {'key_name':'layer_name', 'header_name':'Tensor Layer Name', 'align':'left'}, |
| 345 | + {'key_name':'human_layer_name', 'header_name':'Human Friendly Tensor Layer Name', 'align':'left'}, |
| 346 | + {'key_name':'element_count', 'header_name':'Elements', 'align':'left'}, |
| 347 | + {'key_name':'pretty_dimension', 'header_name':'Shape', 'align':'left'}, |
| 348 | + {'key_name':'tensor_type', 'header_name':'Type', 'align':'left'}, |
| 349 | + ] |
| 350 | + |
| 351 | + markdown_content += markdown_table_with_alignment_support(tensor_dump_table_header_map, tensor_dump_table) |
| 352 | + |
| 353 | + markdown_content += "\n" |
| 354 | + markdown_content += f"- Total elements in {group}: ({element_count_rounded_notation(group_elements):>4}) {group_elements}\n" |
| 355 | + markdown_content += f"- Percentage of total elements: {group_percentage:.2f}%\n" |
| 356 | + markdown_content += "\n\n" |
| 357 | + |
| 358 | + print(markdown_content) # noqa: NP100 |
| 359 | + |
| 360 | + |
104 | 361 | def main() -> None:
|
105 | 362 | parser = argparse.ArgumentParser(description="Dump GGUF file metadata")
|
106 | 363 | parser.add_argument("model", type=str, help="GGUF format model filename")
|
107 | 364 | parser.add_argument("--no-tensors", action="store_true", help="Don't dump tensor metadata")
|
108 | 365 | parser.add_argument("--json", action="store_true", help="Produce JSON output")
|
109 | 366 | parser.add_argument("--json-array", action="store_true", help="Include full array values in JSON output (long)")
|
| 367 | + parser.add_argument("--markdown", action="store_true", help="Produce markdown output") |
110 | 368 | parser.add_argument("--verbose", action="store_true", help="increase output verbosity")
|
111 | 369 |
|
112 | 370 | args = parser.parse_args(None if len(sys.argv) > 1 else ["--help"])
|
113 | 371 |
|
114 | 372 | logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
|
115 | 373 |
|
116 |
| - if not args.json: |
| 374 | + if not args.json and not args.markdown: |
117 | 375 | logger.info(f'* Loading: {args.model}')
|
118 | 376 |
|
119 | 377 | reader = GGUFReader(args.model, 'r')
|
120 | 378 |
|
121 | 379 | if args.json:
|
122 | 380 | dump_metadata_json(reader, args)
|
| 381 | + elif args.markdown: |
| 382 | + dump_markdown_metadata(reader, args) |
123 | 383 | else:
|
124 | 384 | dump_metadata(reader, args)
|
125 | 385 |
|
|
0 commit comments