Step-3.7-Flash-GGUF / scripts /report-imatrix-activations.py
tarruda's picture
Super-squash branch 'main' using huggingface_hub
78005e9
Raw
History Blame Contribute Delete
22 kB
#!/usr/bin/env python3
"""Report FFN activation importance from a llama.cpp imatrix GGUF.
The script ranks FFN down and gate/up activation streams from imatrix tensors.
For MoE tensors, it also tries to infer the routed expert count and report the
highest-activation block/expert pairs.
"""
from __future__ import annotations
import argparse
import math
import re
import sys
from collections import Counter, defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable
import numpy as np
try:
from gguf import GGUFReader
except ImportError as exc: # pragma: no cover - exercised by users without gguf.
raise SystemExit(
"Could not import gguf. Run this with a Python environment that has "
"llama.cpp's gguf package installed, for example:\n"
" ~/code/llama.cpp/.venv/bin/python scripts/report-imatrix-activations.py imatrix.gguf"
) from exc
BLOCK_PATTERNS = (
re.compile(r"^blk\.(\d+)\.(.+)\.weight$"),
re.compile(r"^layers\.(\d+)\.(.+)\.weight$"),
re.compile(r"^model\.layers\.(\d+)\.(.+)\.weight$"),
)
@dataclass(frozen=True)
class Candidate:
base: str
tensor_name: str
count_name: str
block: int | None
module: str
kind: str
projection: str
family: str
n: int
@dataclass
class Row:
block: int | None
family: str
kind: str
projection: str
tensor: str
count: float
n: int
mean_mse: float
rms: float
p95: float
p99: float
max_value: float
gt1: float
gt4: float
@dataclass
class ExpertRow:
block: int
expert: int
family: str
tensor: str
count: float
n: int
mean_mse: float
rms: float
p95: float
p99: float
max_value: float
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Rank FFN down and gate/up activation streams from an imatrix GGUF.",
)
parser.add_argument("imatrix", type=Path, help="Path to an imatrix .gguf file")
parser.add_argument(
"--top",
type=int,
default=15,
help="Rows to show in the top tensor-stream table. Default: 15",
)
parser.add_argument(
"--family-top",
type=int,
default=5,
help="Rows to show per tensor family. Default: 5",
)
parser.add_argument(
"--top-experts",
type=int,
default=25,
help="Rows to show in the combined routed expert table. Default: 25",
)
parser.add_argument(
"--top-expert-streams",
type=int,
default=15,
help="Rows to show in the per-stream routed expert table. Default: 15",
)
parser.add_argument(
"--last-blocks",
type=int,
default=10,
help="Number of final MoE blocks to show in the compact table. Default: 10",
)
parser.add_argument(
"--experts",
type=int,
default=None,
help="Override routed expert count. Useful when it cannot be inferred.",
)
parser.add_argument(
"--no-experts",
action="store_true",
help="Skip per-expert routed rankings.",
)
parser.add_argument(
"--include-router-input",
action="store_true",
help="Include ffn_gate_inp/router input tensors when present.",
)
return parser.parse_args()
def field_type_name(field: object, index: int) -> str:
try:
return field.types[index].name
except Exception:
return ""
def part_to_list(part: object) -> object:
return part.tolist() if hasattr(part, "tolist") else part
def decode_bytes(values: object) -> str:
if isinstance(values, list):
return bytes(values).decode("utf-8", errors="replace")
if isinstance(values, np.ndarray):
return bytes(values.tolist()).decode("utf-8", errors="replace")
if isinstance(values, bytes):
return values.decode("utf-8", errors="replace")
return str(values)
def decode_field(field: object) -> object:
"""Best-effort conversion of a GGUF ReaderField into a printable value."""
if not getattr(field, "parts", None):
return ""
if field_type_name(field, 0) == "STRING":
return decode_bytes(part_to_list(field.parts[-1]))
if field_type_name(field, 0) == "ARRAY" and field_type_name(field, 1) == "STRING":
# llama.cpp imatrix files usually store a single dataset string.
return decode_bytes(part_to_list(field.parts[-1]))
value = part_to_list(field.parts[-1])
if isinstance(value, list) and len(value) == 1:
return value[0]
return value
def parse_block_module(base: str) -> tuple[int | None, str]:
for pattern in BLOCK_PATTERNS:
match = pattern.match(base)
if match:
return int(match.group(1)), match.group(2)
if base.endswith(".weight"):
return None, base[: -len(".weight")]
return None, base
def classify_module(module: str, include_router_input: bool = False) -> tuple[str, str, str] | None:
"""Return (kind, projection, family) for FFN modules that this report handles."""
if module == "ffn_gate_inp" or module.endswith(".ffn_gate_inp"):
if not include_router_input:
return None
return "router", "gate input", "router gate input"
if "ffn_down" in module:
projection = "down"
elif "ffn_gate" in module or "ffn_up" in module:
projection = "gate/up"
else:
return None
if "_exps" in module or ".experts." in module or "_experts" in module:
kind = "routed expert"
elif "_shexp" in module or "shared_expert" in module or "shared_experts" in module:
kind = "shared expert"
else:
kind = "dense"
return kind, projection, f"{kind} {projection}"
def collect_lengths(reader: GGUFReader) -> dict[tuple[int | None, str], int]:
lengths: dict[tuple[int | None, str], int] = {}
for tensor in reader.tensors:
if not tensor.name.endswith(".in_sum2"):
continue
base = tensor.name[: -len(".in_sum2")]
block, module = parse_block_module(base)
lengths[(block, module)] = int(np.asarray(tensor.data).size)
return lengths
def collect_candidates(reader: GGUFReader, include_router_input: bool) -> list[Candidate]:
tensor_names = {tensor.name for tensor in reader.tensors}
candidates: list[Candidate] = []
for tensor in reader.tensors:
if not tensor.name.endswith(".in_sum2"):
continue
base = tensor.name[: -len(".in_sum2")]
count_name = base + ".counts"
if count_name not in tensor_names:
continue
block, module = parse_block_module(base)
classified = classify_module(module, include_router_input=include_router_input)
if classified is None:
continue
kind, projection, family = classified
candidates.append(
Candidate(
base=base,
tensor_name=tensor.name,
count_name=count_name,
block=block,
module=module,
kind=kind,
projection=projection,
family=family,
n=int(np.asarray(tensor.data).size),
)
)
return candidates
def infer_expert_count(candidates: Iterable[Candidate], lengths: dict[tuple[int | None, str], int]) -> tuple[int | None, Counter[int]]:
candidates = list(candidates)
gate_ref_modules = {
"attn_gate",
"attn_k",
"attn_q",
"attn_v",
"ffn_gate",
"ffn_up",
"ffn_gate_shexp",
"ffn_up_shexp",
}
down_ref_modules = {
"ffn_down",
"ffn_down_shexp",
}
gate_refs: dict[int | None, set[int]] = defaultdict(set)
down_refs: dict[int | None, set[int]] = defaultdict(set)
for (block, module), size in lengths.items():
if module in gate_ref_modules:
gate_refs[block].add(size)
if module in down_ref_modules:
down_refs[block].add(size)
# Prefer dimensions discovered from the FFN candidates themselves. This
# keeps inference useful for MoE GGUFs that have shared experts, dense
# leading layers, or non-standard attention tensor availability.
for candidate in candidates:
if candidate.block is None or candidate.kind == "routed expert":
continue
if candidate.projection == "gate/up":
gate_refs[candidate.block].add(candidate.n)
elif candidate.projection == "down":
down_refs[candidate.block].add(candidate.n)
votes: Counter[int] = Counter()
for candidate in candidates:
if candidate.kind != "routed expert" or candidate.block is None:
continue
refs = gate_refs[candidate.block] if candidate.projection == "gate/up" else down_refs[candidate.block]
for ref in refs:
if ref <= 0 or candidate.n % ref != 0:
continue
expert_count = candidate.n // ref
if 1 < expert_count <= 4096:
votes[expert_count] += 1
if not votes:
return None, votes
return votes.most_common(1)[0][0], votes
def deduplicate_gate_up(candidates: Iterable[Candidate]) -> list[Candidate]:
"""Keep one tensor for each shared gate/up input stream."""
by_key: dict[tuple[int | None, str], Candidate] = {}
for candidate in candidates:
key = (candidate.block, candidate.family)
current = by_key.get(key)
if current is None:
by_key[key] = candidate
continue
# Prefer gate over up because it is the conventional representative.
if "ffn_gate" in candidate.module and "ffn_up" in current.module:
by_key[key] = candidate
return list(by_key.values())
def stats(values: np.ndarray) -> tuple[float, float, float, float, float, float, float]:
mean_mse = float(np.mean(values))
rms = math.sqrt(mean_mse) if mean_mse >= 0 and math.isfinite(mean_mse) else float("nan")
return (
mean_mse,
rms,
float(np.percentile(values, 95)),
float(np.percentile(values, 99)),
float(np.max(values)),
float(np.mean(values > 1.0)),
float(np.mean(values > 4.0)),
)
def analyze(reader: GGUFReader, args: argparse.Namespace) -> tuple[list[Row], list[ExpertRow], int | None, Counter[int]]:
tensor_by_name = {tensor.name: tensor for tensor in reader.tensors}
lengths = collect_lengths(reader)
candidates = collect_candidates(reader, include_router_input=args.include_router_input)
inferred_experts, expert_votes = infer_expert_count(candidates, lengths)
expert_count = args.experts if args.experts is not None else inferred_experts
candidates = deduplicate_gate_up(candidates)
rows: list[Row] = []
expert_rows: list[ExpertRow] = []
for candidate in candidates:
count = float(np.asarray(tensor_by_name[candidate.count_name].data).reshape(-1)[0])
raw = np.asarray(tensor_by_name[candidate.tensor_name].data, dtype=np.float64)
values = raw / count if count else np.full_like(raw, np.nan, dtype=np.float64)
mean_mse, rms, p95, p99, max_value, gt1, gt4 = stats(values)
rows.append(
Row(
block=candidate.block,
family=candidate.family,
kind=candidate.kind,
projection=candidate.projection,
tensor=candidate.base,
count=count,
n=candidate.n,
mean_mse=mean_mse,
rms=rms,
p95=p95,
p99=p99,
max_value=max_value,
gt1=gt1,
gt4=gt4,
)
)
if (
not args.no_experts
and expert_count
and candidate.kind == "routed expert"
and candidate.block is not None
and candidate.n % expert_count == 0
):
per_expert = values.reshape(expert_count, candidate.n // expert_count)
for expert_id, expert_values in enumerate(per_expert):
exp_mean, exp_rms, exp_p95, exp_p99, exp_max, _, _ = stats(expert_values)
expert_rows.append(
ExpertRow(
block=candidate.block,
expert=expert_id,
family=candidate.family,
tensor=candidate.base,
count=count,
n=int(expert_values.size),
mean_mse=exp_mean,
rms=exp_rms,
p95=exp_p95,
p99=exp_p99,
max_value=exp_max,
)
)
return rows, expert_rows, expert_count, expert_votes
def fmt(value: object) -> str:
if isinstance(value, float):
if math.isnan(value):
return ""
return f"{value:.4g}"
if isinstance(value, int):
return str(value)
return str(value)
def pct(value: float) -> str:
return f"{100.0 * value:.1f}%"
def block_label(block: int | None) -> str:
return "?" if block is None else str(block)
def print_table(headers: list[str], rows: Iterable[list[object]]) -> None:
print("| " + " | ".join(headers) + " |")
print("| " + " | ".join("---" for _ in headers) + " |")
for row in rows:
print("| " + " | ".join(fmt(value) for value in row) + " |")
def metadata(reader: GGUFReader) -> dict[str, object]:
wanted = ("general.type", "imatrix.datasets", "imatrix.chunk_count", "imatrix.chunk_size")
return {key: decode_field(reader.fields[key]) for key in wanted if key in reader.fields}
def aggregate_experts(expert_rows: Iterable[ExpertRow]) -> list[dict[str, object]]:
acc: dict[tuple[int, int], dict[str, object]] = {}
for row in expert_rows:
key = (row.block, row.expert)
item = acc.setdefault(
key,
{
"block": row.block,
"expert": row.expert,
"weighted_mse_sum": 0.0,
"n": 0,
"max_stream_rms": 0.0,
"streams": [],
},
)
item["weighted_mse_sum"] = float(item["weighted_mse_sum"]) + row.mean_mse * row.n
item["n"] = int(item["n"]) + row.n
item["max_stream_rms"] = max(float(item["max_stream_rms"]), row.rms)
item["streams"].append(row.family)
result: list[dict[str, object]] = []
for item in acc.values():
mean_mse = float(item["weighted_mse_sum"]) / int(item["n"])
result.append(
{
"block": item["block"],
"expert": item["expert"],
"rms": math.sqrt(mean_mse),
"mean_mse": mean_mse,
"max_stream_rms": item["max_stream_rms"],
"streams": ", ".join(sorted(set(item["streams"]))),
}
)
return result
def render(path: Path, reader: GGUFReader, rows: list[Row], expert_rows: list[ExpertRow], expert_count: int | None, expert_votes: Counter[int], args: argparse.Namespace) -> None:
print(f"# {path.name}")
print()
print("Score: `RMS = sqrt(mean(in_sum2 / counts))`.")
print()
meta = metadata(reader)
if meta:
print("Metadata:")
print()
print_table(["Key", "Value"], ([key, value] for key, value in meta.items()))
print()
blocks = sorted(row.block for row in rows if row.block is not None)
if blocks:
print(f"Blocks covered: `{blocks[0]}-{blocks[-1]}`")
print()
if expert_count:
source = "command line" if args.experts is not None else "inferred from tensor lengths"
print(f"Routed expert count: `{expert_count}` ({source}).")
if expert_votes and args.experts is None:
vote_text = ", ".join(f"{count}: {votes}" for count, votes in expert_votes.most_common(5))
print(f"Expert-count inference votes: `{vote_text}`.")
print()
elif not args.no_experts:
print("Routed expert count could not be inferred. Use `--experts N` to enable per-expert rankings.")
print()
rows_sorted = sorted(rows, key=lambda row: row.rms, reverse=True)
if rows_sorted:
print(f"Top {min(args.top, len(rows_sorted))} `down` / `gate/up` tensor streams:")
print()
print_table(
["Block", "Family", "Tensor", "RMS", "Mean MSE", "P95", "Max", "Count"],
(
[
block_label(row.block),
row.family,
f"`{row.tensor}`",
row.rms,
row.mean_mse,
row.p95,
row.max_value,
int(row.count),
]
for row in rows_sorted[: args.top]
),
)
print()
if expert_rows:
print(
"Per-expert rankings assume llama.cpp-style routed expert tensors are flattened "
"with each expert stored contiguously. Counts are tensor-level counts, so these "
"rank aggregate activation contribution per expert stream."
)
print()
aggregated = sorted(aggregate_experts(expert_rows), key=lambda row: float(row["rms"]), reverse=True)
print(f"Top {min(args.top_experts, len(aggregated))} routed experts, combined across routed down and gate/up:")
print()
print_table(
["Block", "Expert", "Combined RMS", "Mean MSE", "Max stream RMS", "Streams"],
(
[
row["block"],
row["expert"],
row["rms"],
row["mean_mse"],
row["max_stream_rms"],
row["streams"],
]
for row in aggregated[: args.top_experts]
),
)
print()
expert_streams = sorted(expert_rows, key=lambda row: row.rms, reverse=True)
print(f"Top {min(args.top_expert_streams, len(expert_streams))} routed expert streams:")
print()
print_table(
["Block", "Expert", "Family", "Tensor", "RMS", "Mean MSE", "P95", "Max", "Count"],
(
[
row.block,
row.expert,
row.family,
f"`{row.tensor}`",
row.rms,
row.mean_mse,
row.p95,
row.max_value,
int(row.count),
]
for row in expert_streams[: args.top_expert_streams]
),
)
print()
families = [
"dense down",
"dense gate/up",
"routed expert down",
"routed expert gate/up",
"shared expert down",
"shared expert gate/up",
"router gate input",
]
print(f"Top {args.family_top} by family:")
print()
family_rows: list[list[object]] = []
for family in families:
subset = [row for row in rows if row.family == family]
for row in sorted(subset, key=lambda item: item.rms, reverse=True)[: args.family_top]:
family_rows.append(
[
family,
block_label(row.block),
f"`{row.tensor}`",
row.rms,
row.mean_mse,
row.p95,
row.p99,
row.max_value,
pct(row.gt1),
pct(row.gt4),
int(row.count),
]
)
print_table(
["Family", "Block", "Tensor", "RMS", "Mean MSE", "P95", "P99", "Max", ">1", ">4", "Count"],
family_rows,
)
print()
moe_blocks = sorted(
{
row.block
for row in rows
if row.block is not None and row.kind in {"routed expert", "shared expert"}
}
)
if args.last_blocks > 0 and moe_blocks:
selected_blocks = moe_blocks[-args.last_blocks :]
by_block_family = {(row.block, row.family): row for row in rows}
print(f"Last {len(selected_blocks)} MoE block RMS:")
print()
compact_rows: list[list[object]] = []
for block in selected_blocks:
compact_rows.append(
[
block,
by_block_family.get((block, "shared expert down"), "").rms
if (block, "shared expert down") in by_block_family
else "",
by_block_family.get((block, "shared expert gate/up"), "").rms
if (block, "shared expert gate/up") in by_block_family
else "",
by_block_family.get((block, "routed expert down"), "").rms
if (block, "routed expert down") in by_block_family
else "",
by_block_family.get((block, "routed expert gate/up"), "").rms
if (block, "routed expert gate/up") in by_block_family
else "",
]
)
print_table(["Block", "Shared down", "Shared gate/up", "Routed down", "Routed gate/up"], compact_rows)
print()
def main() -> int:
args = parse_args()
if not args.imatrix.exists():
print(f"error: file not found: {args.imatrix}", file=sys.stderr)
return 2
reader = GGUFReader(str(args.imatrix))
rows, expert_rows, expert_count, expert_votes = analyze(reader, args)
if not rows:
print("error: no FFN down/gate/up imatrix tensors were found", file=sys.stderr)
return 1
render(args.imatrix, reader, rows, expert_rows, expert_count, expert_votes, args)
return 0
if __name__ == "__main__":
raise SystemExit(main())