code-oracle 0.1.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.
- code_oracle/__init__.py +30 -0
- code_oracle/cli.py +795 -0
- code_oracle/config.py +145 -0
- code_oracle/dataset.py +5325 -0
- code_oracle/dead_code/__init__.py +32 -0
- code_oracle/dead_code/detector.py +379 -0
- code_oracle/dead_code/entrypoints.py +333 -0
- code_oracle/dead_code/models.py +255 -0
- code_oracle/dead_code/semantics.py +416 -0
- code_oracle/decision.py +906 -0
- code_oracle/engine.py +430 -0
- code_oracle/export_onnx.py +436 -0
- code_oracle/hook.py +531 -0
- code_oracle/indexer.py +894 -0
- code_oracle/languages/__init__.py +114 -0
- code_oracle/languages/common.py +127 -0
- code_oracle/languages/go.py +395 -0
- code_oracle/languages/python.py +336 -0
- code_oracle/languages/rust.py +474 -0
- code_oracle/languages/typescript.py +775 -0
- code_oracle/linearizer.py +166 -0
- code_oracle/locator.py +301 -0
- code_oracle/models.py +237 -0
- code_oracle/perf_lint/__init__.py +38 -0
- code_oracle/perf_lint/engine.py +234 -0
- code_oracle/perf_lint/models.py +229 -0
- code_oracle/perf_lint/rules/__init__.py +31 -0
- code_oracle/perf_lint/rules/async_blocking.py +143 -0
- code_oracle/perf_lint/rules/n_plus_one.py +232 -0
- code_oracle/perf_lint/rules/nested_loops.py +137 -0
- code_oracle/perf_lint/rules/unclosed_res.py +494 -0
- code_oracle/perf_lint/visitor.py +299 -0
- code_oracle/server.py +184 -0
- code_oracle/slicer.py +225 -0
- code_oracle/symbolic.py +459 -0
- code_oracle-0.1.0.dist-info/METADATA +225 -0
- code_oracle-0.1.0.dist-info/RECORD +40 -0
- code_oracle-0.1.0.dist-info/WHEEL +4 -0
- code_oracle-0.1.0.dist-info/entry_points.txt +2 -0
- code_oracle-0.1.0.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ONNX Export and Dynamic INT8 Quantization Pipeline for Code Oracle.
|
|
3
|
+
|
|
4
|
+
Exports ModernBERT multi-task decision models to ONNX format with dynamic axes
|
|
5
|
+
and performs dynamic INT8 quantization for sub-50ms CPU inference.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
OUTPUT_NAMES = [
|
|
19
|
+
"risk_score",
|
|
20
|
+
"risk_logits",
|
|
21
|
+
"taxonomy_logits",
|
|
22
|
+
"taxonomy_probs",
|
|
23
|
+
"log_variance",
|
|
24
|
+
"variance",
|
|
25
|
+
"confidence",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class OnnxExportWrapper:
|
|
30
|
+
"""Wrapper to adapt dictionary output to tuple for clean ONNX export."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, base_model: Any):
|
|
33
|
+
super().__init__()
|
|
34
|
+
self.base_model = base_model
|
|
35
|
+
|
|
36
|
+
def __call__(self, input_ids: Any, attention_mask: Any) -> Tuple[Any, ...]:
|
|
37
|
+
return self.forward(input_ids, attention_mask)
|
|
38
|
+
|
|
39
|
+
def forward(self, input_ids: Any, attention_mask: Any) -> Tuple[Any, ...]:
|
|
40
|
+
res = self.base_model(input_ids=input_ids, attention_mask=attention_mask)
|
|
41
|
+
return (
|
|
42
|
+
res["risk_score"],
|
|
43
|
+
res["risk_logits"],
|
|
44
|
+
res["taxonomy_logits"],
|
|
45
|
+
res["taxonomy_probs"],
|
|
46
|
+
res["log_variance"],
|
|
47
|
+
res["variance"],
|
|
48
|
+
res["confidence"],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def export_model_to_onnx(
|
|
53
|
+
weights_path: Union[Path, str],
|
|
54
|
+
output_path: Union[Path, str],
|
|
55
|
+
opset_version: int = 17,
|
|
56
|
+
model: Optional[Any] = None,
|
|
57
|
+
) -> Path:
|
|
58
|
+
"""
|
|
59
|
+
Export ModernBERT multi-task model to ONNX format with dynamic batch & sequence axes.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
weights_path: Directory containing PyTorch model weights (e.g. model.safetensors).
|
|
63
|
+
output_path: Destination file path for model.onnx.
|
|
64
|
+
opset_version: Target ONNX opset version (default: 17).
|
|
65
|
+
model: Optional pre-loaded PyTorch model instance.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Path to exported ONNX model file.
|
|
69
|
+
"""
|
|
70
|
+
import torch
|
|
71
|
+
from safetensors.torch import load_file
|
|
72
|
+
from code_oracle.decision import ModernBERTWithMultiTaskHead
|
|
73
|
+
|
|
74
|
+
weights_path = Path(weights_path)
|
|
75
|
+
output_path = Path(output_path)
|
|
76
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
|
|
78
|
+
if model is None:
|
|
79
|
+
safetensors_file = weights_path / "model.safetensors"
|
|
80
|
+
if not safetensors_file.exists():
|
|
81
|
+
raise FileNotFoundError(f"Model safetensors weights not found at {safetensors_file}")
|
|
82
|
+
|
|
83
|
+
config_file = weights_path / "config.json"
|
|
84
|
+
encoder_name = "answerdotai/ModernBERT-base"
|
|
85
|
+
if config_file.exists():
|
|
86
|
+
try:
|
|
87
|
+
with open(config_file, "r", encoding="utf-8") as f_cfg:
|
|
88
|
+
cfg_data = json.load(f_cfg)
|
|
89
|
+
encoder_name = cfg_data.get("encoder", encoder_name)
|
|
90
|
+
except Exception as e_cfg:
|
|
91
|
+
logger.debug(f"Could not read config.json encoder: {e_cfg}")
|
|
92
|
+
|
|
93
|
+
logger.info(f"Loading PyTorch weights from {safetensors_file} with encoder {encoder_name}")
|
|
94
|
+
model = ModernBERTWithMultiTaskHead(encoder_name=encoder_name)
|
|
95
|
+
sd = load_file(str(safetensors_file))
|
|
96
|
+
model.load_state_dict(sd)
|
|
97
|
+
|
|
98
|
+
model.eval()
|
|
99
|
+
|
|
100
|
+
# Create export wrapper subclassing torch.nn.Module
|
|
101
|
+
class _ExportModule(torch.nn.Module):
|
|
102
|
+
def __init__(self, inner_model: torch.nn.Module):
|
|
103
|
+
super().__init__()
|
|
104
|
+
self.inner = inner_model
|
|
105
|
+
|
|
106
|
+
def forward(self, input_ids: torch.Tensor, attention_mask: torch.Tensor) -> Tuple[torch.Tensor, ...]:
|
|
107
|
+
res = self.inner(input_ids, attention_mask)
|
|
108
|
+
return (
|
|
109
|
+
res["risk_score"],
|
|
110
|
+
res["risk_logits"],
|
|
111
|
+
res["taxonomy_logits"],
|
|
112
|
+
res["taxonomy_probs"],
|
|
113
|
+
res["log_variance"],
|
|
114
|
+
res["variance"],
|
|
115
|
+
res["confidence"],
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
export_module = _ExportModule(model)
|
|
119
|
+
export_module.eval()
|
|
120
|
+
|
|
121
|
+
dummy_input_ids = torch.ones((1, 16), dtype=torch.long)
|
|
122
|
+
dummy_attention_mask = torch.ones((1, 16), dtype=torch.long)
|
|
123
|
+
|
|
124
|
+
dynamic_axes = {
|
|
125
|
+
"input_ids": {0: "batch_size", 1: "sequence_length"},
|
|
126
|
+
"attention_mask": {0: "batch_size", 1: "sequence_length"},
|
|
127
|
+
"risk_score": {0: "batch_size"},
|
|
128
|
+
"risk_logits": {0: "batch_size"},
|
|
129
|
+
"taxonomy_logits": {0: "batch_size"},
|
|
130
|
+
"taxonomy_probs": {0: "batch_size"},
|
|
131
|
+
"log_variance": {0: "batch_size"},
|
|
132
|
+
"variance": {0: "batch_size"},
|
|
133
|
+
"confidence": {0: "batch_size"},
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
logger.info(f"Exporting ONNX model to {output_path} (opset {opset_version})...")
|
|
137
|
+
torch.onnx.export(
|
|
138
|
+
export_module,
|
|
139
|
+
(dummy_input_ids, dummy_attention_mask),
|
|
140
|
+
str(output_path),
|
|
141
|
+
input_names=["input_ids", "attention_mask"],
|
|
142
|
+
output_names=OUTPUT_NAMES,
|
|
143
|
+
dynamic_axes=dynamic_axes,
|
|
144
|
+
opset_version=opset_version,
|
|
145
|
+
dynamo=False,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
logger.info(f"ONNX export completed: {output_path} ({output_path.stat().st_size / (1024 * 1024):.2f} MB)")
|
|
149
|
+
return output_path
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def quantize_onnx_int8(
|
|
153
|
+
onnx_path: Union[Path, str],
|
|
154
|
+
output_path: Union[Path, str],
|
|
155
|
+
per_channel: bool = True,
|
|
156
|
+
reduce_range: bool = True,
|
|
157
|
+
op_types: Optional[List[str]] = None,
|
|
158
|
+
) -> Path:
|
|
159
|
+
"""
|
|
160
|
+
Apply dynamic INT8 quantization to an ONNX model, targeting MatMul / Gemm operations.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
onnx_path: Path to FP32 ONNX model.
|
|
164
|
+
output_path: Path for output INT8 quantized model.
|
|
165
|
+
per_channel: Whether to quantize weights per-channel (default: True).
|
|
166
|
+
reduce_range: Whether to use 7-bit quantization for non-VNNI hardware (default: True for overflow protection).
|
|
167
|
+
op_types: Optional list of operator types to quantize (default: all supported linear ops).
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Path to quantized model_int8.onnx.
|
|
171
|
+
"""
|
|
172
|
+
import onnxruntime.quantization as oq
|
|
173
|
+
|
|
174
|
+
onnx_path = Path(onnx_path)
|
|
175
|
+
output_path = Path(output_path)
|
|
176
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
|
|
178
|
+
if not onnx_path.exists():
|
|
179
|
+
raise FileNotFoundError(f"Source ONNX model not found: {onnx_path}")
|
|
180
|
+
|
|
181
|
+
logger.info(f"Applying dynamic INT8 quantization (per_channel={per_channel}) to {onnx_path}...")
|
|
182
|
+
|
|
183
|
+
quant_kwargs: Dict[str, Any] = {
|
|
184
|
+
"model_input": str(onnx_path),
|
|
185
|
+
"model_output": str(output_path),
|
|
186
|
+
"per_channel": per_channel,
|
|
187
|
+
"reduce_range": reduce_range,
|
|
188
|
+
"weight_type": oq.QuantType.QInt8,
|
|
189
|
+
}
|
|
190
|
+
if op_types:
|
|
191
|
+
quant_kwargs["op_types_to_quantize"] = op_types
|
|
192
|
+
|
|
193
|
+
oq.quantize_dynamic(**quant_kwargs)
|
|
194
|
+
|
|
195
|
+
orig_size = onnx_path.stat().st_size / (1024 * 1024)
|
|
196
|
+
quant_size = output_path.stat().st_size / (1024 * 1024)
|
|
197
|
+
reduction = ((orig_size - quant_size) / orig_size) * 100.0 if orig_size > 0 else 0.0
|
|
198
|
+
logger.info(f"INT8 quantization completed: {output_path} ({quant_size:.2f} MB, {reduction:.1f}% reduction)")
|
|
199
|
+
return output_path
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def verify_numeric_parity(
|
|
203
|
+
weights_path: Union[Path, str],
|
|
204
|
+
onnx_fp32_path: Union[Path, str],
|
|
205
|
+
onnx_int8_path: Optional[Union[Path, str]] = None,
|
|
206
|
+
sample_texts: Optional[List[str]] = None,
|
|
207
|
+
max_diff_fp32: float = 1e-4,
|
|
208
|
+
max_diff_int8: float = 0.05,
|
|
209
|
+
) -> Dict[str, Any]:
|
|
210
|
+
"""
|
|
211
|
+
Verify numeric parity between PyTorch outputs and ONNX (FP32 & INT8) outputs.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
weights_path: Directory with PyTorch weights and tokenizer.
|
|
215
|
+
onnx_fp32_path: Path to model.onnx.
|
|
216
|
+
onnx_int8_path: Optional path to model_int8.onnx.
|
|
217
|
+
sample_texts: Optional list of sample linearized DSL texts.
|
|
218
|
+
max_diff_fp32: Maximum acceptable absolute diff for FP32 ONNX vs PyTorch.
|
|
219
|
+
max_diff_int8: Maximum acceptable absolute diff for INT8 ONNX vs PyTorch.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Structured parity evaluation dictionary.
|
|
223
|
+
"""
|
|
224
|
+
import numpy as np
|
|
225
|
+
import onnxruntime as ort
|
|
226
|
+
import torch
|
|
227
|
+
from safetensors.torch import load_file
|
|
228
|
+
from transformers import AutoTokenizer
|
|
229
|
+
from code_oracle.decision import ModernBERTWithMultiTaskHead
|
|
230
|
+
|
|
231
|
+
weights_path = Path(weights_path)
|
|
232
|
+
onnx_fp32_path = Path(onnx_fp32_path)
|
|
233
|
+
|
|
234
|
+
default_samples = [
|
|
235
|
+
"[DIFF_TARGET] app.py\n[GATE]\nSTATUS: APPROVED\nDEF add(a, b) -> RETURN a + b",
|
|
236
|
+
"[DIFF_TARGET] user_service.py\n[GATE]\nSTATUS: REJECTED\nARITY_MISMATCH: compute() takes 2 arguments but 3 were given",
|
|
237
|
+
"[DIFF_TARGET] auth.py\n[GATE]\nSTATUS: APPROVED\nCALL authenticate(user, password) -> VALID",
|
|
238
|
+
"[DIFF_TARGET] pipeline.py\n[GATE]\nSTATUS: REJECTED\nCIRCULAR_DEPENDENCY: cycle detected [A -> B -> A]",
|
|
239
|
+
]
|
|
240
|
+
samples = sample_texts or default_samples
|
|
241
|
+
|
|
242
|
+
# Load PyTorch model
|
|
243
|
+
sd = load_file(str(weights_path / "model.safetensors"))
|
|
244
|
+
pt_model = ModernBERTWithMultiTaskHead()
|
|
245
|
+
pt_model.load_state_dict(sd)
|
|
246
|
+
pt_model.eval()
|
|
247
|
+
|
|
248
|
+
# Load tokenizer
|
|
249
|
+
tokenizer = AutoTokenizer.from_pretrained(str(weights_path))
|
|
250
|
+
|
|
251
|
+
# Initialize ONNX sessions
|
|
252
|
+
session_fp32 = ort.InferenceSession(str(onnx_fp32_path), providers=["CPUExecutionProvider"])
|
|
253
|
+
session_int8 = None
|
|
254
|
+
if onnx_int8_path and Path(onnx_int8_path).exists():
|
|
255
|
+
session_int8 = ort.InferenceSession(str(onnx_int8_path), providers=["CPUExecutionProvider"])
|
|
256
|
+
|
|
257
|
+
max_fp32_risk_diff = 0.0
|
|
258
|
+
max_fp32_conf_diff = 0.0
|
|
259
|
+
max_fp32_tax_diff = 0.0
|
|
260
|
+
|
|
261
|
+
max_int8_risk_diff = 0.0
|
|
262
|
+
max_int8_conf_diff = 0.0
|
|
263
|
+
max_int8_tax_diff = 0.0
|
|
264
|
+
|
|
265
|
+
sample_results = []
|
|
266
|
+
|
|
267
|
+
for text in samples:
|
|
268
|
+
tokens = tokenizer(text, return_tensors="pt", truncation=True, max_length=512)
|
|
269
|
+
|
|
270
|
+
# PyTorch inference
|
|
271
|
+
with torch.no_grad():
|
|
272
|
+
pt_out = pt_model(tokens["input_ids"], tokens["attention_mask"])
|
|
273
|
+
|
|
274
|
+
pt_risk = float(pt_out["risk_score"].item())
|
|
275
|
+
pt_conf = float(pt_out["confidence"].item())
|
|
276
|
+
pt_tax = [float(x) for x in pt_out["taxonomy_probs"].squeeze(0).tolist()]
|
|
277
|
+
|
|
278
|
+
# ONNX FP32 inference
|
|
279
|
+
ort_inputs = {
|
|
280
|
+
"input_ids": tokens["input_ids"].numpy().astype(np.int64),
|
|
281
|
+
"attention_mask": tokens["attention_mask"].numpy().astype(np.int64),
|
|
282
|
+
}
|
|
283
|
+
fp32_outs = session_fp32.run(OUTPUT_NAMES, ort_inputs)
|
|
284
|
+
fp32_map = dict(zip(OUTPUT_NAMES, fp32_outs))
|
|
285
|
+
|
|
286
|
+
fp32_risk = float(fp32_map["risk_score"][0][0])
|
|
287
|
+
fp32_conf = float(fp32_map["confidence"][0][0])
|
|
288
|
+
fp32_tax = [float(x) for x in fp32_map["taxonomy_probs"][0]]
|
|
289
|
+
|
|
290
|
+
d_fp32_risk = abs(fp32_risk - pt_risk)
|
|
291
|
+
d_fp32_conf = abs(fp32_conf - pt_conf)
|
|
292
|
+
d_fp32_tax = max(abs(a - b) for a, b in zip(fp32_tax, pt_tax))
|
|
293
|
+
|
|
294
|
+
max_fp32_risk_diff = max(max_fp32_risk_diff, d_fp32_risk)
|
|
295
|
+
max_fp32_conf_diff = max(max_fp32_conf_diff, d_fp32_conf)
|
|
296
|
+
max_fp32_tax_diff = max(max_fp32_tax_diff, d_fp32_tax)
|
|
297
|
+
|
|
298
|
+
sample_entry: Dict[str, Any] = {
|
|
299
|
+
"text": text[:40] + "...",
|
|
300
|
+
"pt_risk": pt_risk,
|
|
301
|
+
"fp32_risk": fp32_risk,
|
|
302
|
+
"fp32_risk_diff": d_fp32_risk,
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
# ONNX INT8 inference if available
|
|
306
|
+
if session_int8 is not None:
|
|
307
|
+
int8_outs = session_int8.run(OUTPUT_NAMES, ort_inputs)
|
|
308
|
+
int8_map = dict(zip(OUTPUT_NAMES, int8_outs))
|
|
309
|
+
|
|
310
|
+
int8_risk = float(int8_map["risk_score"][0][0])
|
|
311
|
+
int8_conf = float(int8_map["confidence"][0][0])
|
|
312
|
+
int8_tax = [float(x) for x in int8_map["taxonomy_probs"][0]]
|
|
313
|
+
|
|
314
|
+
d_int8_risk = abs(int8_risk - pt_risk)
|
|
315
|
+
d_int8_conf = abs(int8_conf - pt_conf)
|
|
316
|
+
d_int8_tax = max(abs(a - b) for a, b in zip(int8_tax, pt_tax))
|
|
317
|
+
|
|
318
|
+
max_int8_risk_diff = max(max_int8_risk_diff, d_int8_risk)
|
|
319
|
+
max_int8_conf_diff = max(max_int8_conf_diff, d_int8_conf)
|
|
320
|
+
max_int8_tax_diff = max(max_int8_tax_diff, d_int8_tax)
|
|
321
|
+
|
|
322
|
+
sample_entry["int8_risk"] = int8_risk
|
|
323
|
+
sample_entry["int8_risk_diff"] = d_int8_risk
|
|
324
|
+
|
|
325
|
+
sample_results.append(sample_entry)
|
|
326
|
+
|
|
327
|
+
fp32_pass = (
|
|
328
|
+
max_fp32_risk_diff <= max_diff_fp32
|
|
329
|
+
and max_fp32_conf_diff <= max_diff_fp32
|
|
330
|
+
and max_fp32_tax_diff <= max_diff_fp32
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
int8_pass = True
|
|
334
|
+
if session_int8 is not None:
|
|
335
|
+
int8_pass = (
|
|
336
|
+
max_int8_risk_diff <= max_diff_int8
|
|
337
|
+
and max_int8_conf_diff <= max_diff_int8
|
|
338
|
+
and max_int8_tax_diff <= max_diff_int8
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
"status": "PASS" if (fp32_pass and int8_pass) else "FAIL",
|
|
343
|
+
"fp32_parity_pass": fp32_pass,
|
|
344
|
+
"int8_parity_pass": int8_pass,
|
|
345
|
+
"max_fp32_risk_diff": max_fp32_risk_diff,
|
|
346
|
+
"max_fp32_conf_diff": max_fp32_conf_diff,
|
|
347
|
+
"max_fp32_tax_diff": max_fp32_tax_diff,
|
|
348
|
+
"max_int8_risk_diff": max_int8_risk_diff,
|
|
349
|
+
"max_int8_conf_diff": max_int8_conf_diff,
|
|
350
|
+
"max_int8_tax_diff": max_int8_tax_diff,
|
|
351
|
+
"samples_tested": len(samples),
|
|
352
|
+
"sample_results": sample_results,
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def export_and_quantize(
|
|
357
|
+
weights_path: Union[Path, str],
|
|
358
|
+
output_dir: Optional[Union[Path, str]] = None,
|
|
359
|
+
quantize_int8: bool = True,
|
|
360
|
+
verify_parity: bool = True,
|
|
361
|
+
opset_version: int = 17,
|
|
362
|
+
) -> Dict[str, Any]:
|
|
363
|
+
"""
|
|
364
|
+
End-to-end pipeline: Export PyTorch weights to ONNX FP32 and dynamic INT8,
|
|
365
|
+
verify numeric parity, and synchronize configuration and tokenizer files.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
weights_path: Source directory containing PyTorch weights (e.g. weights_base).
|
|
369
|
+
output_dir: Output directory (defaults to weights_path if None).
|
|
370
|
+
quantize_int8: Whether to generate model_int8.onnx.
|
|
371
|
+
verify_parity: Whether to run numeric parity verification against PyTorch.
|
|
372
|
+
opset_version: Target ONNX opset version.
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
Structured dictionary summarizing export results.
|
|
376
|
+
"""
|
|
377
|
+
weights_path = Path(weights_path).resolve()
|
|
378
|
+
out_dir = Path(output_dir or weights_path).resolve()
|
|
379
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
380
|
+
|
|
381
|
+
t0 = time.perf_counter()
|
|
382
|
+
|
|
383
|
+
# 1. Export FP32 ONNX
|
|
384
|
+
onnx_fp32_path = out_dir / "model.onnx"
|
|
385
|
+
export_model_to_onnx(
|
|
386
|
+
weights_path=weights_path,
|
|
387
|
+
output_path=onnx_fp32_path,
|
|
388
|
+
opset_version=opset_version,
|
|
389
|
+
)
|
|
390
|
+
fp32_size_mb = onnx_fp32_path.stat().st_size / (1024 * 1024)
|
|
391
|
+
|
|
392
|
+
# 2. Dynamic INT8 Quantization
|
|
393
|
+
onnx_int8_path = None
|
|
394
|
+
int8_size_mb = None
|
|
395
|
+
if quantize_int8:
|
|
396
|
+
onnx_int8_path = out_dir / "model_int8.onnx"
|
|
397
|
+
quantize_onnx_int8(
|
|
398
|
+
onnx_path=onnx_fp32_path,
|
|
399
|
+
output_path=onnx_int8_path,
|
|
400
|
+
)
|
|
401
|
+
int8_size_mb = onnx_int8_path.stat().st_size / (1024 * 1024)
|
|
402
|
+
|
|
403
|
+
# 3. Synchronize ancillary assets (config.json, tokenizer.json, tokenizer_config.json)
|
|
404
|
+
if out_dir != weights_path:
|
|
405
|
+
for fname in ["config.json", "tokenizer.json", "tokenizer_config.json"]:
|
|
406
|
+
src_f = weights_path / fname
|
|
407
|
+
if src_f.exists():
|
|
408
|
+
shutil.copy2(src_f, out_dir / fname)
|
|
409
|
+
# Also copy tokenizer dir if present
|
|
410
|
+
tok_dir = weights_path / "tokenizer"
|
|
411
|
+
if tok_dir.is_dir() and not (out_dir / "tokenizer").exists():
|
|
412
|
+
shutil.copytree(tok_dir, out_dir / "tokenizer")
|
|
413
|
+
|
|
414
|
+
# 4. Numeric Parity Verification
|
|
415
|
+
parity_report = None
|
|
416
|
+
if verify_parity:
|
|
417
|
+
logger.info("Running numeric parity verification against PyTorch...")
|
|
418
|
+
parity_report = verify_numeric_parity(
|
|
419
|
+
weights_path=weights_path,
|
|
420
|
+
onnx_fp32_path=onnx_fp32_path,
|
|
421
|
+
onnx_int8_path=onnx_int8_path,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
elapsed_s = time.perf_counter() - t0
|
|
425
|
+
|
|
426
|
+
result = {
|
|
427
|
+
"status": "SUCCESS",
|
|
428
|
+
"output_dir": str(out_dir),
|
|
429
|
+
"model_onnx": str(onnx_fp32_path),
|
|
430
|
+
"model_int8_onnx": str(onnx_int8_path) if onnx_int8_path else None,
|
|
431
|
+
"fp32_size_mb": round(fp32_size_mb, 2),
|
|
432
|
+
"int8_size_mb": round(int8_size_mb, 2) if int8_size_mb is not None else None,
|
|
433
|
+
"elapsed_seconds": round(elapsed_s, 2),
|
|
434
|
+
"parity": parity_report,
|
|
435
|
+
}
|
|
436
|
+
return result
|