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.
Files changed (40) hide show
  1. code_oracle/__init__.py +30 -0
  2. code_oracle/cli.py +795 -0
  3. code_oracle/config.py +145 -0
  4. code_oracle/dataset.py +5325 -0
  5. code_oracle/dead_code/__init__.py +32 -0
  6. code_oracle/dead_code/detector.py +379 -0
  7. code_oracle/dead_code/entrypoints.py +333 -0
  8. code_oracle/dead_code/models.py +255 -0
  9. code_oracle/dead_code/semantics.py +416 -0
  10. code_oracle/decision.py +906 -0
  11. code_oracle/engine.py +430 -0
  12. code_oracle/export_onnx.py +436 -0
  13. code_oracle/hook.py +531 -0
  14. code_oracle/indexer.py +894 -0
  15. code_oracle/languages/__init__.py +114 -0
  16. code_oracle/languages/common.py +127 -0
  17. code_oracle/languages/go.py +395 -0
  18. code_oracle/languages/python.py +336 -0
  19. code_oracle/languages/rust.py +474 -0
  20. code_oracle/languages/typescript.py +775 -0
  21. code_oracle/linearizer.py +166 -0
  22. code_oracle/locator.py +301 -0
  23. code_oracle/models.py +237 -0
  24. code_oracle/perf_lint/__init__.py +38 -0
  25. code_oracle/perf_lint/engine.py +234 -0
  26. code_oracle/perf_lint/models.py +229 -0
  27. code_oracle/perf_lint/rules/__init__.py +31 -0
  28. code_oracle/perf_lint/rules/async_blocking.py +143 -0
  29. code_oracle/perf_lint/rules/n_plus_one.py +232 -0
  30. code_oracle/perf_lint/rules/nested_loops.py +137 -0
  31. code_oracle/perf_lint/rules/unclosed_res.py +494 -0
  32. code_oracle/perf_lint/visitor.py +299 -0
  33. code_oracle/server.py +184 -0
  34. code_oracle/slicer.py +225 -0
  35. code_oracle/symbolic.py +459 -0
  36. code_oracle-0.1.0.dist-info/METADATA +225 -0
  37. code_oracle-0.1.0.dist-info/RECORD +40 -0
  38. code_oracle-0.1.0.dist-info/WHEEL +4 -0
  39. code_oracle-0.1.0.dist-info/entry_points.txt +2 -0
  40. 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