addyo07/query-scope-classifier
0
1#!/usr/bin/env python32"""3Layer 3: ONNX INT8 Export & Confidence Threshold Calibration (tau*)4Model: ModernBERT-base fine-tuned on 22,006 Golden Dataset samples5Output ONNX: /opt/vox/sandbox/artifacts/memory_scope_multilingual_int8.onnx6"""7 8import os9import sys10import json11import time12import torch13import numpy as np14import pandas as pd15import onnx16import onnxruntime as ort17from onnxruntime.quantization import quantize_dynamic, QuantType18from transformers import AutoTokenizer, AutoModelForSequenceClassification19from sklearn.metrics import accuracy_score, precision_recall_fscore_support20from sklearn.model_selection import train_test_split21 22GOLDEN_DATASET_PATH = "/opt/vox/sandbox/datasets/memory_scope_golden_v1.json"23PYTORCH_MODEL_DIR = "/opt/vox/sandbox/artifacts/modernbert_scope_final/final_pytorch_model"24FP32_ONNX_PATH = "/opt/vox/sandbox/artifacts/memory_scope_fp32.onnx"25INT8_ONNX_PATH = "/opt/vox/sandbox/artifacts/memory_scope_multilingual_int8.onnx"26RESULTS_DIR = "/opt/vox/sandbox/results"27 28SCOPE_MAP = {"ChitChat": 0, "User": 1, "Domain": 2, "Temporal": 3}29ID_TO_SCOPE = {0: "ChitChat", 1: "User", 2: "Domain", 3: "Temporal"}30DOMAIN_CLASS_ID = 231 32def softmax(logits):33 exp_z = np.exp(logits - np.max(logits, axis=-1, keepdims=True))34 return exp_z / np.sum(exp_z, axis=-1, keepdims=True)35 36def main():37 print("=== Layer 3: ONNX INT8 Export & Confidence Threshold Calibration Pipeline ===", flush=True)38 39 # 1. Load Fine-Tuned PyTorch Model & Tokenizer40 print("\n--- Phase 3.1: ONNX FP32 Export & INT8 Dynamic Quantization ---", flush=True)41 tokenizer = AutoTokenizer.from_pretrained(PYTORCH_MODEL_DIR)42 model = AutoModelForSequenceClassification.from_pretrained(PYTORCH_MODEL_DIR)43 model.eval()44 45 # Dummy input for ONNX export46 dummy_text = "Fix Tokio deadlock in module A"47 dummy_inputs = tokenizer(dummy_text, return_tensors="pt", max_length=64, truncation=True, padding="max_length")48 49 print(f"Exporting PyTorch model to FP32 ONNX at {FP32_ONNX_PATH}...", flush=True)50 torch.onnx.export(51 model,52 (dummy_inputs["input_ids"], dummy_inputs["attention_mask"]),53 FP32_ONNX_PATH,54 input_names=["input_ids", "attention_mask"],55 output_names=["logits"],56 dynamic_axes={57 "input_ids": {0: "batch_size", 1: "sequence_length"},58 "attention_mask": {0: "batch_size", 1: "sequence_length"},59 "logits": {0: "batch_size"}60 },61 opset_version=18,62 dynamo=False63 )64 65 fp32_size_mb = os.path.getsize(FP32_ONNX_PATH) / (1024 * 1024)66 print(f"FP32 ONNX Model File Size: {fp32_size_mb:.2f} MB", flush=True)67 68 # Quantize FP32 to INT869 print(f"Quantizing FP32 ONNX to INT8 ONNX at {INT8_ONNX_PATH}...", flush=True)70 quantize_dynamic(71 model_input=FP32_ONNX_PATH,72 model_output=INT8_ONNX_PATH,73 weight_type=QuantType.QUInt8,74 )75 76 int8_size_mb = os.path.getsize(INT8_ONNX_PATH) / (1024 * 1024)77 print(f"INT8 ONNX Model File Size: {int8_size_mb:.2f} MB", flush=True)78 79 # 2. Load Holdout Test Split80 with open(GOLDEN_DATASET_PATH, "r", encoding="utf-8") as f:81 samples = json.load(f)["samples"]82 83 formatted_data = [84 {85 "id": s["id"],86 "text": s["text"],87 "label": SCOPE_MAP[s["scope"]],88 "language": s.get("language", "en"),89 "strat_key": f"{s['scope']}_{s.get('language', 'en')}"90 }91 for s in samples92 ]93 df = pd.DataFrame(formatted_data)94 _, temp_df = train_test_split(df, test_size=0.20, random_state=42, stratify=df["strat_key"])95 _, test_df = train_test_split(temp_df, test_size=0.50, random_state=42, stratify=temp_df["strat_key"])96 97 print(f"\n--- Phase 3.2: Confidence Threshold Calibration on {len(test_df)} Holdout Samples ---", flush=True)98 99 session_options = ort.SessionOptions()100 session_options.intra_op_num_threads = 1101 session_options.inter_op_num_threads = 1102 session = ort.InferenceSession(INT8_ONNX_PATH, session_options, providers=["CPUExecutionProvider"])103 104 all_logits = []105 all_labels = test_df["label"].values106 107 start_time = time.time()108 for text in test_df["text"].values:109 enc = tokenizer(text, truncation=True, max_length=64, return_tensors="np")110 inp = {111 "input_ids": enc["input_ids"].astype(np.int64),112 "attention_mask": enc["attention_mask"].astype(np.int64)113 }114 out = session.run(None, inp)115 all_logits.append(out[0][0])116 117 total_time_ms = (time.time() - start_time) * 1000118 avg_latency_ms = total_time_ms / len(test_df)119 print(f"Single-Thread CPU Inference Speed: {avg_latency_ms:.2f} ms per sample.", flush=True)120 121 all_logits = np.array(all_logits)122 all_probs = softmax(all_logits)123 raw_preds = np.argmax(all_probs, axis=-1)124 125 raw_acc = accuracy_score(all_labels, raw_preds)126 print(f"Raw INT8 ONNX Test Accuracy (Uncalibrated): {raw_acc*100:.2f}%", flush=True)127 128 # Sweep Threshold tau129 best_tau = 0.50130 best_non_default_prec = 0.0131 best_calibrated_acc = 0.0132 calibration_records = []133 134 print("\nSweeping Confidence Threshold tau in range [0.50, 0.98]:", flush=True)135 print(f"{'tau':<8} | {'Calib Acc':<10} | {'Non-Default Prec':<20} | {'Fallback Rate':<15}", flush=True)136 print("-" * 60, flush=True)137 138 for tau in np.arange(0.50, 0.99, 0.01):139 calibrated_preds = []140 fallback_count = 0141 142 for probs in all_probs:143 max_p = np.max(probs)144 raw_c = np.argmax(probs)145 146 # If highest confidence prediction is non-default and below tau, fall back to Domain (Primary Default)147 if raw_c != DOMAIN_CLASS_ID and max_p < tau:148 calibrated_preds.append(DOMAIN_CLASS_ID)149 fallback_count += 1150 else:151 calibrated_preds.append(raw_c)152 153 calibrated_preds = np.array(calibrated_preds)154 calib_acc = accuracy_score(all_labels, calibrated_preds)155 156 # Calculate Non-Default Precision (Precision on ChitChat, User, Temporal)157 precision_per_class, _, _, _ = precision_recall_fscore_support(158 all_labels, calibrated_preds, average=None, labels=[0, 1, 2, 3], zero_division=0159 )160 non_default_prec = (precision_per_class[0] + precision_per_class[1] + precision_per_class[3]) / 3.0161 fallback_rate = (fallback_count / len(test_df)) * 100162 163 print(f"{tau:<8.2f} | {calib_acc*100:<10.2f}% | {non_default_prec*100:<20.2f}% | {fallback_rate:<15.2f}%", flush=True)164 165 calibration_records.append({166 "tau": float(tau),167 "calib_accuracy": float(calib_acc),168 "non_default_precision": float(non_default_prec),169 "fallback_rate": float(fallback_rate),170 "precision_chitchat": float(precision_per_class[0]),171 "precision_user": float(precision_per_class[1]),172 "precision_domain": float(precision_per_class[2]),173 "precision_temporal": float(precision_per_class[3]),174 })175 176 if non_default_prec >= 0.98 and (best_calibrated_acc == 0.0 or calib_acc > best_calibrated_acc):177 best_tau = tau178 best_non_default_prec = non_default_prec179 best_calibrated_acc = calib_acc180 181 # If no tau reached 98% non-default precision, pick tau that maximizes non-default precision182 if best_non_default_prec < 0.98:183 sorted_records = sorted(calibration_records, key=lambda x: x["non_default_precision"], reverse=True)184 best_rec = sorted_records[0]185 best_tau = best_rec["tau"]186 best_non_default_prec = best_rec["non_default_precision"]187 best_calibrated_acc = best_rec["calib_accuracy"]188 189 print("\n" + "="*66, flush=True)190 print(f"๐ฏ OPTIMAL CALIBRATED THRESHOLD tau* = {best_tau:.2f}", flush=True)191 print(f" - Calibrated Test Accuracy: {best_calibrated_acc*100:.2f}%", flush=True)192 print(f" - Non-Default Label Precision: {best_non_default_prec*100:.2f}% (Target: โฅ98.0%)", flush=True)193 print(f" - INT8 ONNX File Size: {int8_size_mb:.2f} MB", flush=True)194 print(f" - Single-Thread CPU Latency: {avg_latency_ms:.2f} ms/sample (SLA: 10-30 ms)", flush=True)195 print("="*66, flush=True)196 197 calibration_payload = {198 "best_tau": best_tau,199 "best_calibrated_accuracy": best_calibrated_acc,200 "best_non_default_precision": best_non_default_prec,201 "int8_file_size_mb": int8_size_mb,202 "avg_cpu_latency_ms": avg_latency_ms,203 "sweep_records": calibration_records204 }205 with open(os.path.join(RESULTS_DIR, "threshold_calibration_results.json"), "w") as f:206 json.dump(calibration_payload, f, indent=2)207 208 layer3_passed = (best_non_default_prec >= 0.98) and (avg_latency_ms <= 30.0)209 print(f"\n๐ฏ LAYER 3 MILESTONE VERDICT: {'โ
PASSED' if layer3_passed else 'โ FAILED'}", flush=True)210 211if __name__ == "__main__":212 main()213 