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 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()