File size: 10,003 Bytes
c99f13f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
import heapq
import warnings
from functools import lru_cache
from typing import Dict, List, Set, Tuple, Any
from dataclasses import dataclass, field

from constants import (
    TIER_ORDER, TIER_BPW, GGUF_OVERHEAD_FACTOR, CLASS_MAX_TIER,
    CAN_Q3, ALLOW_LOWER_FLOOR, MTP_DEPLOY_TIER, get_tensor_class, get_tensor_type,
    is_mtp_tensor,
)

# Выносим делитель в константу (8 бит * 1024 байт * 1024 кбайт)
BITS_IN_MIB = 8 * 1024 * 1024.0

# Предвычисляем множители размеров для каждого тира (ускорение математики)
TIER_SIZE_MULTIPLIER = {
    tier: (bpw / BITS_IN_MIB) * GGUF_OVERHEAD_FACTOR 
    for tier, bpw in TIER_BPW.items()
}

# K-quants используют блоки по 256 элементов и требуют выравнивания (padding) 3D-тензоров MoE
K_QUANTS = {"Q3_K", "Q4_K", "Q5_K", "Q6_K"}
MOE_PAD_TYPES = {"ffn_gate_exps", "ffn_up_exps", "ffn_down_exps", "ffn_down"}

MSE_BPW = {
    "IQ1_S": 1.5625, "IQ2_XXS": 2.0625, "IQ2_XS": 2.3125,
    "IQ2_S": 2.5,
    "IQ3_XXS": 3.0625,
    "Q3_K": 3.4375, "IQ3_S": 3.44,
    "IQ4_XS": 4.25, "IQ4_NL": 4.25, "Q4_K": 4.50,
    "Q5_K": 5.50, "Q6_K": 6.5625, "Q8_0": 8.50, "F16": 16.0,
}

@dataclass(order=True, slots=True)
class UpgradeItem:
    """Элемент очереди апгрейдов. Сравнивается только по neg_utility."""
    neg_utility: float
    group_id: int = field(compare=False)
    next_tier: str = field(compare=False)
    cost_delta: float = field(compare=False)


def _tier_index(tier: str) -> int:
    if tier not in TIER_ORDER:
        raise ValueError(f"Unknown tier: {tier}")
    return TIER_ORDER.index(tier)

def _tier_at(idx: int) -> str:
    if not (0 <= idx < len(TIER_ORDER)):
        raise IndexError(f"Tier index {idx} out of range")
    return TIER_ORDER[idx]

def _size_mib(tier: str, n_elements: int) -> float:
    """Размер тензора в MiB с учётом оверхеда GGUF."""
    if n_elements <= 0:
        return 0.0
    return n_elements * TIER_SIZE_MULTIPLIER.get(tier, 0.0)

@lru_cache(maxsize=64)
def _mse_delta(cur_tier: str, next_tier: str) -> float:
    return (2 ** (-2 * MSE_BPW[cur_tier])) - (2 ** (-2 * MSE_BPW[next_tier]))

def _push_upgrade(group_id: int,
                  group_registry: Dict[int, Tuple[List[str], int, int]],
                  assignments: Dict[str, str],
                  tensor_importance: Dict[str, float],
                  upgrade_queue: List[UpgradeItem],
                  importance_table: Dict[str, Any]):
    
    # Храним 3 элемента: имена, реальный размер, размер с padding
    g_names, g_elements, g_elements_padded = group_registry[group_id]
    rep_name = g_names[0]
    cur_tier = assignments[rep_name]
    cur_idx = _tier_index(cur_tier)

    rep_info = importance_table.get(rep_name, {})
    ttype = rep_info["type"] if "type" in rep_info else get_tensor_type(rep_name)
    cls = get_tensor_class(ttype)
    max_tier = CLASS_MAX_TIER.get(cls, "Q8_0")

    if cur_idx >= _tier_index(max_tier) or cur_idx >= len(TIER_ORDER) - 1:
        return

    next_tier = _tier_at(cur_idx + 1)
    
    # Выбираем размер в зависимости от того, относится ли тир к K-quants
    cur_size_g = g_elements_padded if cur_tier in K_QUANTS else g_elements
    next_size_g = g_elements_padded if next_tier in K_QUANTS else g_elements
    
    cost_delta = _size_mib(next_tier, next_size_g) - _size_mib(cur_tier, cur_size_g)

    quality_delta = _mse_delta(cur_tier, next_tier)
    if quality_delta <= 0:
        return

    if cost_delta == 0:
        utility_per_mb = float('inf')
    elif cost_delta < 0:
        return 
    else:
        total_g_imp = sum(tensor_importance.get(n, 0) for n in g_names)
        utility_per_mb = (total_g_imp * quality_delta) / cost_delta

    heapq.heappush(upgrade_queue, UpgradeItem(-utility_per_mb, group_id, next_tier, cost_delta))


def compute_initial_assignments(non_mtp_names: Set[str], mtp_names: Set[str], 
                                importance_table: Dict, allow_q3: bool, is_qat: bool = False) -> Dict[str, str]:
    assignments = {name: MTP_DEPLOY_TIER for name in mtp_names}

    for name in non_mtp_names:
        rep_info = importance_table.get(name, {})
        has_imatrix = "importance_mean" in rep_info
        ttype = rep_info["type"] if "type" in rep_info else get_tensor_type(name)
        cls = get_tensor_class(ttype)
        
        if cls in ("norms", "ssm_params"):
            assignments[name] = "F16"
        elif cls == "embd":
            assignments[name] = "Q4_K" if is_qat else "Q5_K"
        else:
            base_floor = ("Q4_K" if cls == "attn_proj" else "IQ4_XS") if is_qat else "Q4_K"
            if allow_q3 and cls in CAN_Q3 and has_imatrix:
                assignments[name] = ALLOW_LOWER_FLOOR
            else:
                assignments[name] = base_floor
            
    return assignments


def build_groups(tied_groups: List[List[str]], non_mtp_names: Set[str], 
                 ne_map: Dict[str, int], padded_ne_map: Dict[str, int]) -> Dict[int, Tuple[List[str], int, int]]:
    group_registry = {}
    assigned_tensors = set()
    
    for group_idx, group in enumerate(tied_groups):
        clean_group = [n for n in group if n in non_mtp_names]
        if clean_group:
            g_elements = sum(ne_map.get(n, 0) for n in clean_group)
            g_elements_padded = sum(padded_ne_map.get(n, 0) for n in clean_group)
            group_registry[group_idx] = (clean_group, g_elements, g_elements_padded)
            assigned_tensors.update(clean_group)

    unassigned_tensors = non_mtp_names - assigned_tensors
    next_group_idx = len(group_registry)
    
    for name in unassigned_tensors:
        group_registry[next_group_idx] = ([name], ne_map.get(name, 0), padded_ne_map.get(name, 0))
        next_group_idx += 1
            
    return group_registry


def optimal_classify(importance_table: dict, tied_groups: list, model: dict, 
                     target_size_mib: float, allow_q3: bool = False) -> Tuple[dict, dict]:
    if target_size_mib <= 0:
        raise ValueError("target_size_mib must be positive")

    features = model.get("features", {})
    has_mtp = features.get("has_mtp", False)
    n_layers = features.get("n_layers", 31)
    is_qat = features.get("is_qat", False)
    model_tensors = model.get("tensors", {})

    ne_map = {k: v["n_elements"] for k, v in model_tensors.items()}
    for tname, info in importance_table.items():
        if tname not in ne_map:
            ne_map[tname] = info["n_elements"]

    # --- Вычисление MoE Padding (Целочисленное выравнивание) ---
    moe_d_ff = features.get("moe_intermediate_size", 0)
    padded_ne_map = dict(ne_map)
    if moe_d_ff > 0 and moe_d_ff % 256 != 0:
        aligned_d_ff = ((moe_d_ff + 255) // 256) * 256
        for name, n_el in ne_map.items():
            ttype = importance_table.get(name, {}).get("type", get_tensor_type(name))
            if ttype in MOE_PAD_TYPES:
                padded_ne_map[name] = (n_el // moe_d_ff) * aligned_d_ff
    # -----------------------------------------------------------

    all_names = set(ne_map.keys())
    mtp_names = {n for n in all_names if is_mtp_tensor(n, n_layers)} if has_mtp else set()
    non_mtp_names = all_names - mtp_names

    tensor_importance = {}
    for name in non_mtp_names:
        raw_imp = importance_table.get(name, {}).get("importance_mean", 0.0)
        tensor_importance[name] = raw_imp

    assignments = compute_initial_assignments(non_mtp_names, mtp_names, importance_table, allow_q3, is_qat)
    group_registry = build_groups(tied_groups, non_mtp_names, ne_map, padded_ne_map)

    mtp_cost = sum(_size_mib(MTP_DEPLOY_TIER, ne_map.get(n, 0)) for n in mtp_names)
    effective_target = target_size_mib - mtp_cost
    
    current_size = sum(
        _size_mib(assignments[n], padded_ne_map.get(n, ne_map.get(n, 0)) if assignments[n] in K_QUANTS else ne_map.get(n, 0))
        for n in non_mtp_names
    )

    if current_size > effective_target:
        warnings.warn(f"Initial size {current_size:.1f} MiB already exceeds target {effective_target:.1f} MiB", RuntimeWarning)

    upgrade_queue = []
    for g_id in group_registry:
        _push_upgrade(g_id, group_registry, assignments, tensor_importance, upgrade_queue, importance_table)

    while upgrade_queue:
        item = heapq.heappop(upgrade_queue)
        g_id, next_tier, cost_delta = item.group_id, item.next_tier, item.cost_delta
        
        if cost_delta > 0 and current_size + cost_delta > effective_target:
            continue
            
        for n in group_registry[g_id][0]:
            assignments[n] = next_tier
        current_size += cost_delta
        
        _push_upgrade(g_id, group_registry, assignments, tensor_importance, upgrade_queue, importance_table)

    return assignments, padded_ne_map


def compute_stats(assignments: dict, ne_map: dict = None, padded_ne_map: dict = None) -> dict:
    """Собирает статистику по тирам с точным учётом K_QUANTS padding."""
    stats = {"by_tier_count": {}, "by_tier_mib": {}, "total_mib": 0.0, "tensor_count": 0}
    for name, tier in assignments.items():
        if not isinstance(tier, str):
            continue
        stats["tensor_count"] += 1
        stats["by_tier_count"][tier] = stats["by_tier_count"].get(tier, 0) + 1
        
        if ne_map:
            if padded_ne_map and tier in K_QUANTS:
                elements = padded_ne_map.get(name, ne_map.get(name, 0))
            else:
                elements = ne_map.get(name, 0)
                
            size = _size_mib(tier, elements)
            stats["by_tier_mib"][tier] = stats["by_tier_mib"].get(tier, 0.0) + size
            stats["total_mib"] += size
            
    return stats