File size: 5,775 Bytes
491cce7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
#!/usr/bin/env python3
"""
Sensitivity sweep: vary one tensor class tier, measure PPL.
Usage: python sensitivity_sweep.py --model M.gguf --imatrix I.gguf --class ffn_down --tiers Q3_K,IQ4_XS,Q4_K,Q5_K,Q6_K,Q8_0
"""

import argparse
import json
import os
import subprocess
import sys

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from model_reader import read_model
from imatrix_reader import read_imatrix, detect_tied_groups, build_importance_table
from config_generator import generate_flags, format_flags
from quantizer import run_dry_run, run_quantization
from constants import (
    TIER_ORDER, get_tensor_class, strip_weight
)

REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
LLAMA_PPL = os.environ.get(
    "LLAMA_PPL", "/home/wepiqx/llama.cpp/build/bin/llama-perplexity"
)
WIKITEXT_DATA = os.environ.get(
    "WIKITEXT_DATA", "/mnt/Vsio/wikitext-2-raw/wiki.test.raw"
)


def parse_tiers(tier_str):
    return [t.strip() for t in tier_str.split(",")]


def _tier_index(tier: str) -> int:
    return TIER_ORDER.index(tier) if tier in TIER_ORDER else -1


def build_assignments(model, target_class, tier, base_tier="Q5_K"):
    """Manually build assignments: all at base_tier, target_class at tier."""
    model_tensors = model.get("tensors", {})
    all_names = set()
    for tname in model_tensors:
        norm = strip_weight(tname)
        all_names.add(norm)

    assignments = {}
    for name in all_names:
        ttype = name.split(".")[-1] if "." in name else name
        cls = get_tensor_class(ttype)
        if cls == target_class:
            assignments[name] = tier
        else:
            assignments[name] = base_tier

    # Special: norms/ssm_params always F16
    for name in list(assignments.keys()):
        ttype = name.split(".")[-1] if "." in name else name
        cls = get_tensor_class(ttype)
        if cls in ("norms", "ssm_params"):
            assignments[name] = "F16"
        # MTP special handling
        if cls == "mtp":
            assignments[name] = base_tier
        # Embedding - keep at base_tier
        if cls == "embd":
            assignments[name] = base_tier

    return assignments


def main():
    parser = argparse.ArgumentParser(description="Sensitivity sweep for one tensor class")
    parser.add_argument("--model", required=True)
    parser.add_argument("--imatrix", required=True)
    parser.add_argument("--class", dest="target_class", required=True,
                        choices=["gate", "attn_proj", "ffn_gate_up", "ffn_down", "mtp", "embd"])
    parser.add_argument("--tiers", default="Q3_K,IQ4_XS,Q4_K,Q5_K,Q6_K,Q8_0")
    parser.add_argument("--base-tier", default="Q5_K")
    parser.add_argument("--output-dir", default=None,
                        help="Defaults to <repo>/output/sensitivity_sweep")
    parser.add_argument("--skip-ppl", action="store_true", help="Only dry-run, skip PPL")
    args = parser.parse_args()

    tiers = parse_tiers(args.tiers)
    if args.output_dir is None:
        args.output_dir = os.path.join(REPO_ROOT, "output", "sensitivity_sweep")
    os.makedirs(args.output_dir, exist_ok=True)

    print(f"=== Sensitivity sweep: {args.target_class} ===")
    print(f"Tiers: {tiers}")
    print(f"Base tier for others: {args.base_tier}")

    model = read_model(args.model)
    imatrix = read_imatrix(args.imatrix)
    tied_groups = detect_tied_groups(imatrix)
    imp_table = build_importance_table(imatrix, model)

    results = []

    for tier in tiers:
        print(f"\n--- Testing {args.target_class} = {tier} ---")
        
        assignments = build_assignments(model, args.target_class, tier, args.base_tier)

        base_type = "Q5_K_M"
        flags = generate_flags(assignments, model, args.base_tier, 100000)
        flags["imatrix"] = args.imatrix

        # Dry run
        dry_size = run_dry_run(flags, args.model)
        print(f"  Dry-run size: {dry_size:.0f} MiB")

        if args.skip_ppl:
            results.append({"tier": tier, "size": dry_size, "ppl": None})
            continue

        # Quantize (CPU only to avoid OOM)
        out_path = os.path.join(args.output_dir, f"{args.target_class}_{tier}.gguf")
        print(f"  Quantizing to {out_path}...")
        
        # Use CPU for quantization to avoid GPU OOM
        success = run_quantization(flags, args.model, out_path)
        if not success:
            print(f"  FAILED")
            results.append({"tier": tier, "size": dry_size, "ppl": None, "error": "quant failed"})
            continue

        # PPL test (CPU)
        print(f"  Running PPL test (CPU)...")
        cmd = [
            LLAMA_PPL,
            "-m", out_path,
            "-f", WIKITEXT_DATA,
            "-c", "1024", "-ngl", "0"  # CPU only
        ]
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=1800)
        
        ppl = None
        for line in result.stdout.split("\n"):
            if "Final estimate: PPL" in line:
                try:
                    ppl = float(line.split("PPL = ")[1].split(" ")[0])
                except:
                    pass
        
        print(f"  PPL: {ppl}")
        results.append({"tier": tier, "size": dry_size, "ppl": ppl})

        # Cleanup
        if os.path.exists(out_path):
            os.remove(out_path)

    # Summary
    print("\n=== RESULTS ===")
    print(f"{'Tier':<10} {'Size (MiB)':<12} {'PPL':<10}")
    for r in results:
        print(f"{r['tier']:<10} {r['size']:<12.0f} {r['ppl'] if r['ppl'] else 'FAILED':<10}")

    out_json = os.path.join(args.output_dir, f"sensitivity_{args.target_class}.json")
    with open(out_json, "w") as f:
        json.dump(results, f, indent=2)
    print(f"\nSaved: {out_json}")


if __name__ == "__main__":
    main()