gevva 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- decision_engine.py +159 -0
- export_w4a16.py +335 -0
- gemma4_cross_encoder.py +1581 -0
- gevva/__init__.py +110 -0
- gevva/cli.py +196 -0
- gevva-1.0.0.dist-info/METADATA +402 -0
- gevva-1.0.0.dist-info/RECORD +11 -0
- gevva-1.0.0.dist-info/WHEEL +5 -0
- gevva-1.0.0.dist-info/entry_points.txt +2 -0
- gevva-1.0.0.dist-info/top_level.txt +5 -0
- nli_labels.py +20 -0
decision_engine.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""decision_engine.py
|
|
3
|
+
======================
|
|
4
|
+
Unified runtime contract and data models for System 1 Decision Engines.
|
|
5
|
+
|
|
6
|
+
Provides a single standard interface (`System1Engine`) implemented across
|
|
7
|
+
all model paradigms:
|
|
8
|
+
1. `Gemma4CrossEncoder` (Pointwise & Grouped Listwise Cross-Encoder)
|
|
9
|
+
2. `Gemma4CausalDecider` (In-Context Causal Next-Token Decider)
|
|
10
|
+
3. `Gemma4SetAttentionDecider` (Permutation-Equivariant Set Transformer Head)
|
|
11
|
+
|
|
12
|
+
Every engine supports dual operating modes:
|
|
13
|
+
- `mode="fast"`: Minimal latency, single-pass forward evaluation (10-25ms).
|
|
14
|
+
- `mode="robust"`: Maximum quality, permutation-debiased / cyclic-ensembled evaluation (25-80ms).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from abc import ABC, abstractmethod
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class DecisionResult:
|
|
26
|
+
"""Standard result for a K-way categorical choice."""
|
|
27
|
+
best_option: str
|
|
28
|
+
best_index: int
|
|
29
|
+
probabilities: Dict[str, float]
|
|
30
|
+
scores: List[float]
|
|
31
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def top_confidence(self) -> float:
|
|
35
|
+
return self.probabilities.get(self.best_option, 0.0)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class JudgeResult:
|
|
40
|
+
"""Standard result for binary or 3-class fact/policy verification."""
|
|
41
|
+
verdict: str # "entailment", "contradiction", or "neutral"
|
|
42
|
+
probabilities: Dict[str, float] # {"entailment": p1, "contradiction": p2, "neutral": p3}
|
|
43
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def is_entailed(self) -> bool:
|
|
47
|
+
return self.verdict == "entailment"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class RateResult:
|
|
52
|
+
"""Standard result for ordinal rubric grading and expected value scoring."""
|
|
53
|
+
expected_score: float # sum_i(value_i * P(level_i))
|
|
54
|
+
level_probabilities: Dict[str, float]
|
|
55
|
+
predicted_level: str
|
|
56
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class RerankItem:
|
|
61
|
+
"""Single item in a reranking result."""
|
|
62
|
+
index: int
|
|
63
|
+
document: str
|
|
64
|
+
score: float
|
|
65
|
+
probability: float
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class RerankResult:
|
|
70
|
+
"""Ranked list of documents or candidates."""
|
|
71
|
+
ranked_indices: List[int]
|
|
72
|
+
items: List[RerankItem]
|
|
73
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class System1Engine(ABC):
|
|
77
|
+
"""Abstract Base Class for all System 1 Decision Engines."""
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
@abstractmethod
|
|
81
|
+
def model_name(self) -> str:
|
|
82
|
+
"""Identifier of the underlying model or checkpoint."""
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
@abstractmethod
|
|
87
|
+
def paradigm(self) -> str:
|
|
88
|
+
"""Architectural paradigm: 'cross_encoder', 'in_context_causal', or 'set_attention'."""
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
def decide(
|
|
93
|
+
self,
|
|
94
|
+
context: str,
|
|
95
|
+
question: str,
|
|
96
|
+
options: Sequence[str],
|
|
97
|
+
mode: str = "fast",
|
|
98
|
+
**kwargs: Any,
|
|
99
|
+
) -> DecisionResult:
|
|
100
|
+
"""Select the best candidate option given a context and question.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
context: The premise, document, policy text, or evidence.
|
|
104
|
+
question: The decision query, intent, or instruction.
|
|
105
|
+
options: List of K candidate actions, tools, or multiple-choice answers.
|
|
106
|
+
mode: "fast" (single pass) or "robust" (permutation-averaged / cyclic ensembled).
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
DecisionResult with chosen option, index, probabilities, and scores.
|
|
110
|
+
"""
|
|
111
|
+
...
|
|
112
|
+
|
|
113
|
+
@abstractmethod
|
|
114
|
+
def judge(
|
|
115
|
+
self,
|
|
116
|
+
context: str,
|
|
117
|
+
claim: str,
|
|
118
|
+
**kwargs: Any,
|
|
119
|
+
) -> JudgeResult:
|
|
120
|
+
"""Verify whether a declarative claim is supported, contradicted, or unverifiable.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
context: The premise or source text.
|
|
124
|
+
claim: The hypothesis or assertion to verify.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
JudgeResult with verdict and calibrated probability distribution.
|
|
128
|
+
"""
|
|
129
|
+
...
|
|
130
|
+
|
|
131
|
+
@abstractmethod
|
|
132
|
+
def rate(
|
|
133
|
+
self,
|
|
134
|
+
context: str,
|
|
135
|
+
rubric_levels: Sequence[str],
|
|
136
|
+
level_values: Optional[Sequence[float]] = None,
|
|
137
|
+
**kwargs: Any,
|
|
138
|
+
) -> RateResult:
|
|
139
|
+
"""Grade a state against ordered rubric criteria using continuous expected value.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
context: The text or work product being graded.
|
|
143
|
+
rubric_levels: Descriptions of ordered performance levels [Level 0, Level 1, ...].
|
|
144
|
+
level_values: Numeric values for each level (default: 0.0, 1.0, 2.0, ...).
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
RateResult with continuous expected score and per-level probabilities.
|
|
148
|
+
"""
|
|
149
|
+
...
|
|
150
|
+
|
|
151
|
+
@abstractmethod
|
|
152
|
+
def rerank(
|
|
153
|
+
self,
|
|
154
|
+
query: str,
|
|
155
|
+
documents: Sequence[str],
|
|
156
|
+
**kwargs: Any,
|
|
157
|
+
) -> RerankResult:
|
|
158
|
+
"""Rerank candidate passages or documents by relevance / entailment to query."""
|
|
159
|
+
...
|
export_w4a16.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""export_w4a16.py
|
|
3
|
+
==================
|
|
4
|
+
Standalone W4A16 (INT4 Group-32 Weights) Checkpoint Exporter for Gevva Decision Engine.
|
|
5
|
+
|
|
6
|
+
Takes a base Gemma 4 model and fine-tuned LoRA adapter, merges them,
|
|
7
|
+
and packs target linear layers into an INT4 (Group-32 symmetric) compressed weight format.
|
|
8
|
+
|
|
9
|
+
Format Specification:
|
|
10
|
+
1. Merges LoRA adapters directly into base weights before discretization.
|
|
11
|
+
2. In-Features 2D Group Partitioning (dim=1) with group_size=32.
|
|
12
|
+
3. Offset-Binary [0, 15] INT4 Packing: 8 signed 4-bit integers with +8 bias packed into 1 INT32 word.
|
|
13
|
+
4. Preserves 16-bit fidelity on MQA projections (k_proj, v_proj), SigLIP vision tower, and classification head.
|
|
14
|
+
5. Reduces checkpoint disk footprint by ~72% (from ~18 GB to ~5.1 GB).
|
|
15
|
+
6. Note: Dequantizes to BF16 in VRAM at inference time via this repository's native loader;
|
|
16
|
+
it is an optimized weight-storage format, not an external runtime kernel (vLLM/TensorRT).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import shutil
|
|
25
|
+
import time
|
|
26
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
import torch
|
|
30
|
+
import torch.nn as nn
|
|
31
|
+
from safetensors.torch import load_file, save_file
|
|
32
|
+
from transformers import AutoConfig, AutoTokenizer
|
|
33
|
+
from peft import PeftModel
|
|
34
|
+
|
|
35
|
+
from gemma4_cross_encoder import (
|
|
36
|
+
CONTRADICTION,
|
|
37
|
+
ENTAILMENT,
|
|
38
|
+
NEUTRAL,
|
|
39
|
+
ID2LABEL,
|
|
40
|
+
LABEL2ID,
|
|
41
|
+
Gemma4CrossEncoder,
|
|
42
|
+
Gemma4ForSequenceClassification,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def pack_weights_w4a16(
|
|
47
|
+
weight: torch.Tensor,
|
|
48
|
+
group_size: int = 32,
|
|
49
|
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
50
|
+
"""Quantizes and packs a 2D weight matrix into INT4 Group-32 format.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
weight: Float/Bfloat tensor of shape `(out_features, in_features)`.
|
|
54
|
+
group_size: Group size along in_features (dim=1). Must divide in_features.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
packed_weight: INT32 tensor of shape `(out_features, in_features // 8)`.
|
|
58
|
+
scales: BF16 tensor of shape `(out_features, in_features // group_size)`.
|
|
59
|
+
"""
|
|
60
|
+
weight = torch.nan_to_num(weight, nan=0.0, posinf=7.0, neginf=-8.0)
|
|
61
|
+
out_features, in_features = weight.shape
|
|
62
|
+
assert in_features % group_size == 0, f"in_features ({in_features}) must be divisible by group_size ({group_size})"
|
|
63
|
+
assert group_size % 8 == 0, f"group_size ({group_size}) must be divisible by 8 for INT32 packing"
|
|
64
|
+
|
|
65
|
+
# 1. Group-wise symmetric scale calculation (dim=1)
|
|
66
|
+
w_grouped = weight.view(out_features, in_features // group_size, group_size)
|
|
67
|
+
max_abs = torch.amax(torch.abs(w_grouped), dim=-1) + 1e-8 # (out_features, n_groups)
|
|
68
|
+
q_max = 7.0 # 4-bit signed symmetric range [-8, 7]
|
|
69
|
+
scales = (max_abs / q_max).to(torch.bfloat16)
|
|
70
|
+
|
|
71
|
+
# 2. Integer quantization: clamp to [-8, 7]
|
|
72
|
+
scales_expanded = scales.unsqueeze(-1).expand(-1, -1, group_size).reshape(out_features, in_features)
|
|
73
|
+
scales_expanded = torch.clamp_min(scales_expanded, 1e-7)
|
|
74
|
+
w_int = torch.clamp(torch.round(weight / scales_expanded), -8, 7).to(torch.int32)
|
|
75
|
+
|
|
76
|
+
# 3. Standard Offset-Binary [0, 15] for compressed-tensors / vLLM (Offset +8)
|
|
77
|
+
w_u4 = (w_int + 8).to(torch.int32)
|
|
78
|
+
w_chunks = w_u4.view(out_features, in_features // 8, 8)
|
|
79
|
+
packed_weight = torch.zeros((out_features, in_features // 8), dtype=torch.int32, device=weight.device)
|
|
80
|
+
for k in range(8):
|
|
81
|
+
packed_weight |= ((w_chunks[:, :, k].to(torch.int32) & 0x0F) << (4 * k))
|
|
82
|
+
|
|
83
|
+
return packed_weight, scales
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def unpack_weights_w4a16(
|
|
87
|
+
packed_weight: torch.Tensor,
|
|
88
|
+
scales: torch.Tensor,
|
|
89
|
+
group_size: int = 32,
|
|
90
|
+
dtype: torch.dtype = torch.bfloat16,
|
|
91
|
+
) -> torch.Tensor:
|
|
92
|
+
"""Unpacks INT4 Group-32 packed tensor back to reconstructed floating-point weights.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
packed_weight: INT32 tensor of shape `(out_features, in_features // 8)`.
|
|
96
|
+
scales: Float/Bfloat tensor of shape `(out_features, in_features // group_size)`.
|
|
97
|
+
group_size: Quantization group size.
|
|
98
|
+
dtype: Output reconstructed tensor dtype.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
reconstructed: Dequantized tensor of shape `(out_features, in_features)`.
|
|
102
|
+
"""
|
|
103
|
+
out_features, packed_in = packed_weight.shape
|
|
104
|
+
in_features = packed_in * 8
|
|
105
|
+
|
|
106
|
+
# Unpack 8 int4 words from each int32 word
|
|
107
|
+
unpacked_u4 = torch.zeros((out_features, packed_in, 8), dtype=torch.int32, device=packed_weight.device)
|
|
108
|
+
for k in range(8):
|
|
109
|
+
unpacked_u4[:, :, k] = (packed_weight >> (4 * k)) & 0x0F
|
|
110
|
+
|
|
111
|
+
# Invert offset-binary (+8) to recover signed [-8, 7]
|
|
112
|
+
unpacked_signed = (unpacked_u4 - 8).to(dtype)
|
|
113
|
+
w_reconstructed = unpacked_signed.view(out_features, in_features)
|
|
114
|
+
|
|
115
|
+
# Apply per-group scaling
|
|
116
|
+
scales_expanded = scales.unsqueeze(-1).expand(-1, -1, group_size).reshape(out_features, in_features).to(dtype)
|
|
117
|
+
return w_reconstructed * scales_expanded
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def export_model_w4a16(
|
|
121
|
+
base_model_id: str,
|
|
122
|
+
adapter_path: str,
|
|
123
|
+
output_dir: str,
|
|
124
|
+
group_size: int = 32,
|
|
125
|
+
preserve_mqa: bool = True,
|
|
126
|
+
verify: bool = True,
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Loads base model + adapter, merges LoRA, quantizes linear layers, and saves W4A16 safetensors.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
base_model_id: HuggingFace foundation model identifier.
|
|
132
|
+
adapter_path: Directory containing fine-tuned PEFT adapter.
|
|
133
|
+
output_dir: Output directory for packed checkpoint.
|
|
134
|
+
group_size: Weight grouping size (default: 32).
|
|
135
|
+
preserve_mqa: Keep k_proj and v_proj in 16-bit for single-KV-head models.
|
|
136
|
+
verify: Run verification on reconstructed model.
|
|
137
|
+
"""
|
|
138
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
139
|
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
140
|
+
|
|
141
|
+
print("=" * 65)
|
|
142
|
+
print("W4A16 Model Exporter (compressed-tensors format)")
|
|
143
|
+
print("=" * 65)
|
|
144
|
+
print(f"Base Model: {base_model_id}")
|
|
145
|
+
print(f"Adapter Path: {adapter_path}")
|
|
146
|
+
print(f"Output Directory: {output_dir}")
|
|
147
|
+
print(f"Group Size: {group_size}")
|
|
148
|
+
print(f"Preserve MQA: {preserve_mqa}")
|
|
149
|
+
print(f"Device: {device}")
|
|
150
|
+
|
|
151
|
+
# 1. Load Base Model and LoRA Adapter
|
|
152
|
+
print("\n1. Loading base architecture & LoRA weights...")
|
|
153
|
+
config = AutoConfig.from_pretrained(base_model_id)
|
|
154
|
+
config.num_labels = 3
|
|
155
|
+
base_model = Gemma4ForSequenceClassification.from_pretrained(
|
|
156
|
+
base_model_id,
|
|
157
|
+
config=config,
|
|
158
|
+
torch_dtype=torch.bfloat16,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
peft_model = PeftModel.from_pretrained(base_model, adapter_path)
|
|
162
|
+
|
|
163
|
+
# Restore explicit classification head weights if available
|
|
164
|
+
head_weights_path = os.path.join(adapter_path, "head_weights.pt")
|
|
165
|
+
if os.path.exists(head_weights_path):
|
|
166
|
+
print(f"Restoring classification head from {head_weights_path}...")
|
|
167
|
+
hw = torch.load(head_weights_path, map_location="cpu", weights_only=True)
|
|
168
|
+
raw = peft_model.base_model.model if hasattr(peft_model, "base_model") else peft_model
|
|
169
|
+
if "score" in hw and hasattr(raw, "score"):
|
|
170
|
+
target_score = raw.score.modules_to_save["default"] if (hasattr(raw.score, "modules_to_save") and "default" in raw.score.modules_to_save) else raw.score
|
|
171
|
+
if "weight" in hw["score"]:
|
|
172
|
+
target_score.load_state_dict(hw["score"])
|
|
173
|
+
else:
|
|
174
|
+
raw.score.load_state_dict(hw["score"])
|
|
175
|
+
if "norm" in hw and hasattr(raw, "norm"):
|
|
176
|
+
target_norm = raw.norm.modules_to_save["default"] if (hasattr(raw.norm, "modules_to_save") and "default" in raw.norm.modules_to_save) else raw.norm
|
|
177
|
+
if "weight" in hw["norm"]:
|
|
178
|
+
target_norm.load_state_dict(hw["norm"])
|
|
179
|
+
else:
|
|
180
|
+
raw.norm.load_state_dict(hw["norm"])
|
|
181
|
+
|
|
182
|
+
# 2. Merge LoRA weights into base parameters
|
|
183
|
+
print("2. Merging LoRA adapters into base model weights...")
|
|
184
|
+
merged_model = peft_model.merge_and_unload()
|
|
185
|
+
|
|
186
|
+
# Clean up any active PyTorch parametrizations to restore clean state_dict keys
|
|
187
|
+
import torch.nn.utils.parametrize as parametrize
|
|
188
|
+
for name, mod in merged_model.named_modules():
|
|
189
|
+
if hasattr(mod, "parametrizations") and "weight" in mod.parametrizations:
|
|
190
|
+
parametrize.remove_parametrizations(mod, "weight", leave_parametrized=True)
|
|
191
|
+
|
|
192
|
+
merged_model.eval()
|
|
193
|
+
|
|
194
|
+
# 3. Quantize and pack linear layers
|
|
195
|
+
print("\n3. Quantizing target layers to 4-bit INT4 Group-32...")
|
|
196
|
+
state_dict = merged_model.state_dict()
|
|
197
|
+
packed_state_dict: Dict[str, torch.Tensor] = {}
|
|
198
|
+
quantized_modules: List[str] = []
|
|
199
|
+
preserved_modules: List[str] = []
|
|
200
|
+
|
|
201
|
+
for key, tensor in state_dict.items():
|
|
202
|
+
is_quantizable = (
|
|
203
|
+
tensor.ndim == 2
|
|
204
|
+
and "layers" in key
|
|
205
|
+
and "weight" in key
|
|
206
|
+
and any(proj in key for proj in ("self_attn", "mlp"))
|
|
207
|
+
and not any(skip in key for skip in ("vision_tower", "embed_vision", "score", "norm"))
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if preserve_mqa and any(k in key for k in ("k_proj", "v_proj")):
|
|
211
|
+
is_quantizable = False
|
|
212
|
+
|
|
213
|
+
if is_quantizable and tensor.shape[1] % group_size == 0:
|
|
214
|
+
packed_w, scales = pack_weights_w4a16(tensor.cpu(), group_size=group_size)
|
|
215
|
+
# compressed-tensors naming convention:
|
|
216
|
+
# weight -> weight_packed (int32), weight_scale (bfloat16)
|
|
217
|
+
base_key = key[: -len(".weight")]
|
|
218
|
+
packed_state_dict[f"{base_key}.weight_packed"] = packed_w
|
|
219
|
+
packed_state_dict[f"{base_key}.weight_scale"] = scales
|
|
220
|
+
packed_state_dict[f"{base_key}.weight_shape"] = torch.tensor(
|
|
221
|
+
[tensor.shape[0], tensor.shape[1]], dtype=torch.int32
|
|
222
|
+
)
|
|
223
|
+
quantized_modules.append(base_key)
|
|
224
|
+
else:
|
|
225
|
+
packed_state_dict[key] = tensor.to(torch.bfloat16).cpu()
|
|
226
|
+
preserved_modules.append(key)
|
|
227
|
+
|
|
228
|
+
print(f"Quantized and packed: {len(quantized_modules)} linear layers into W4A16.")
|
|
229
|
+
print(f"Preserved in 16-bit: {len(preserved_modules)} parameters (MQA, ViT, Head, Norms, Biases).")
|
|
230
|
+
|
|
231
|
+
# 4. Save Safetensors Checkpoint
|
|
232
|
+
output_weights_file = os.path.join(output_dir, "model.safetensors")
|
|
233
|
+
print(f"\n4. Saving packed weights to {output_weights_file}...")
|
|
234
|
+
save_file(packed_state_dict, output_weights_file)
|
|
235
|
+
|
|
236
|
+
# Calculate compressed file size
|
|
237
|
+
size_mb = os.path.getsize(output_weights_file) / (1024 * 1024)
|
|
238
|
+
print(f"Model saved successfully! Total size: {size_mb:.1f} MB (~{size_mb/1024:.2f} GB)")
|
|
239
|
+
|
|
240
|
+
# 5. Write Quantization Configuration (compressed-tensors metadata)
|
|
241
|
+
quant_config = {
|
|
242
|
+
"quant_method": "compressed-tensors",
|
|
243
|
+
"format": "pack-quantized",
|
|
244
|
+
"config_groups": {
|
|
245
|
+
"group_0": {
|
|
246
|
+
"weights": {
|
|
247
|
+
"num_bits": 4,
|
|
248
|
+
"type": "int",
|
|
249
|
+
"symmetric": True,
|
|
250
|
+
"strategy": "group",
|
|
251
|
+
"group_size": group_size,
|
|
252
|
+
"actorder": None,
|
|
253
|
+
},
|
|
254
|
+
"targets": ["Linear"],
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
"ignore": [
|
|
258
|
+
"*vision_tower*",
|
|
259
|
+
"*embed_vision*",
|
|
260
|
+
"*k_proj*",
|
|
261
|
+
"*v_proj*",
|
|
262
|
+
"*score*",
|
|
263
|
+
"*norm*",
|
|
264
|
+
],
|
|
265
|
+
}
|
|
266
|
+
with open(os.path.join(output_dir, "quantization_config.json"), "w") as f:
|
|
267
|
+
json.dump(quant_config, f, indent=2)
|
|
268
|
+
|
|
269
|
+
# Copy tokenizer and model config
|
|
270
|
+
print("5. Saving tokenizer and configuration files...")
|
|
271
|
+
tokenizer = AutoTokenizer.from_pretrained(adapter_path)
|
|
272
|
+
tokenizer.save_pretrained(output_dir)
|
|
273
|
+
|
|
274
|
+
config.num_labels = 3
|
|
275
|
+
config.id2label = ID2LABEL
|
|
276
|
+
config.label2id = LABEL2ID
|
|
277
|
+
config.quantization_config = quant_config
|
|
278
|
+
config.save_pretrained(output_dir)
|
|
279
|
+
|
|
280
|
+
# Copy calibration and evaluation metadata if present
|
|
281
|
+
for meta_file in ["calibration.json", "eval_metrics.json", "train_config.json"]:
|
|
282
|
+
src_meta = os.path.join(adapter_path, meta_file)
|
|
283
|
+
if os.path.exists(src_meta):
|
|
284
|
+
shutil.copy2(src_meta, os.path.join(output_dir, meta_file))
|
|
285
|
+
print(f" Copied {meta_file} from adapter to {output_dir}.")
|
|
286
|
+
|
|
287
|
+
# 6. Verification & Dequantization Self-Test
|
|
288
|
+
if verify:
|
|
289
|
+
print("\n6. Running dequantization reconstruction verification...")
|
|
290
|
+
sample_layer = quantized_modules[0]
|
|
291
|
+
packed_w = packed_state_dict[f"{sample_layer}.weight_packed"]
|
|
292
|
+
scales = packed_state_dict[f"{sample_layer}.weight_scale"]
|
|
293
|
+
original_w = state_dict[f"{sample_layer}.weight"]
|
|
294
|
+
|
|
295
|
+
reconstructed_w = unpack_weights_w4a16(packed_w, scales, group_size=group_size)
|
|
296
|
+
abs_diff = torch.abs(original_w - reconstructed_w)
|
|
297
|
+
mean_abs_err = torch.mean(abs_diff).item()
|
|
298
|
+
max_abs_err = torch.max(abs_diff).item()
|
|
299
|
+
scales_exp = scales.unsqueeze(-1).expand(-1, -1, group_size).reshape(original_w.shape)
|
|
300
|
+
max_step_err = torch.max(abs_diff / (scales_exp + 1e-8)).item()
|
|
301
|
+
|
|
302
|
+
print(f"Layer [{sample_layer}]:")
|
|
303
|
+
print(f" Mean Absolute Quantization Error: {mean_abs_err:.6f}")
|
|
304
|
+
print(f" Max Absolute Quantization Error: {max_abs_err:.6f}")
|
|
305
|
+
print(f" Max Relative Step Error: {max_step_err:.4f} steps (bound <= 0.6)")
|
|
306
|
+
assert max_step_err <= 0.6, f"Quantization step error exceeded bound: {max_step_err}"
|
|
307
|
+
print("Reconstruction Verification: PASSED!")
|
|
308
|
+
|
|
309
|
+
print("\n" + "=" * 65)
|
|
310
|
+
print(f"Export Complete! W4A16 Artifact ready at: {output_dir}")
|
|
311
|
+
print("=" * 65)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def main():
|
|
315
|
+
parser = argparse.ArgumentParser(description="Export Gemma 4 Cross-Encoder to W4A16 compressed format.")
|
|
316
|
+
parser.add_argument("--base-model", default="google/gemma-4-E2B", help="Base HuggingFace model")
|
|
317
|
+
parser.add_argument("--adapter-path", default="./ckpt/gemma-4-e2b-nli-qat-stage1/best", help="Fine-tuned LoRA adapter path")
|
|
318
|
+
parser.add_argument("--output-dir", default="./ckpt/gemma-4-e2b-nli-w4a16", help="Output directory")
|
|
319
|
+
parser.add_argument("--group-size", type=int, default=32, help="Quantization group size (default: 32)")
|
|
320
|
+
parser.add_argument("--no-preserve-mqa", action="store_false", dest="preserve_mqa", help="Quantize MQA projections as well")
|
|
321
|
+
parser.add_argument("--skip-verify", action="store_false", dest="verify", help="Skip reconstruction verification")
|
|
322
|
+
args = parser.parse_args()
|
|
323
|
+
|
|
324
|
+
export_model_w4a16(
|
|
325
|
+
base_model_id=args.base_model,
|
|
326
|
+
adapter_path=args.adapter_path,
|
|
327
|
+
output_dir=args.output_dir,
|
|
328
|
+
group_size=args.group_size,
|
|
329
|
+
preserve_mqa=args.preserve_mqa,
|
|
330
|
+
verify=args.verify,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
if __name__ == "__main__":
|
|
335
|
+
main()
|