178 lines
7.3 KiB
Python
178 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Static INT8 quantization for bge-reranker-v2-m3 cross-encoder.
|
|
|
|
Dynamic quantization kept weights as fp32 (2725MB). Static quantization
|
|
with calibration data actually reduces weights to INT8 (~500MB).
|
|
|
|
Uses onnxruntime quantization with representative calibration dataset
|
|
of (query, document) pairs covering the tokenizer vocabulary.
|
|
|
|
Usage: docker exec <container> python3 scripts/quantize_reranker_static.py
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
|
|
MODEL_NAME = "BAAI/bge-reranker-v2-m3"
|
|
ONNX_DIR = "/app/data/models/bge-reranker-v2-m3-onnx"
|
|
CALIBRATION_SIZE = 200 # number of calibration samples
|
|
|
|
|
|
def create_calibration_reader(tokenizer, max_seq_len=512):
|
|
"""Create a representative calibration dataset."""
|
|
# Realistic (query, document) pairs for crypto/security domain
|
|
calibration_pairs = [
|
|
(
|
|
"What is a rug pull?",
|
|
"A rug pull is a type of cryptocurrency scam where developers abandon a project after taking investor funds. The liquidity is removed from the pool, making the token worthless.",
|
|
),
|
|
(
|
|
"How to detect honeypot tokens?",
|
|
"Honeypot tokens are smart contracts that allow buying but prevent selling. Detection involves checking for transfer restrictions, blacklist functions, and trading enable/disable mechanisms.",
|
|
),
|
|
(
|
|
"What are common scam patterns?",
|
|
"Common crypto scam patterns include: fake token launches with locked liquidity, impersonation of legitimate projects, phishing sites mimicking popular DEXs, and pump and dump schemes on low-cap tokens.",
|
|
),
|
|
(
|
|
"Explain flash loan attacks",
|
|
"Flash loan attacks exploit uncollateralized lending by manipulating oracle prices within a single transaction. Attackers borrow large amounts, manipulate markets, extract value, and repay within one block.",
|
|
),
|
|
(
|
|
"What is a smart contract audit?",
|
|
"A smart contract audit is a security review of blockchain code to identify vulnerabilities, bugs, and centralization risks. Auditors check for reentrancy, overflow, access control, and logic flaws.",
|
|
),
|
|
(
|
|
"How does MEV work?",
|
|
"Maximal Extractable Value (MEV) refers to profits extracted by reordering, inserting, or censoring transactions within a block. Sandwich attacks, frontrunning, and arbitrage are common MEV strategies.",
|
|
),
|
|
(
|
|
"What are wash trading indicators?",
|
|
"Wash trading indicators include: circular token flows, self-trading patterns, simultaneous buy/sell orders from linked wallets, and volume spikes without corresponding on-chain activity.",
|
|
),
|
|
(
|
|
"Explain ERC-20 token standard",
|
|
"ERC-20 is the standard interface for fungible tokens on Ethereum. It defines transfer, approve, and allowance functions. Common vulnerabilities include unlimited approval and mint functions without caps.",
|
|
),
|
|
(
|
|
"What is a proxy contract?",
|
|
"A proxy contract delegates calls to an implementation contract using delegatecall. This enables upgradeable contracts but introduces risks like storage collisions and unauthorized upgrades.",
|
|
),
|
|
(
|
|
"How to analyze wallet clusters?",
|
|
"Wallet cluster analysis groups addresses by shared funding sources, behavioral patterns, and transaction timing. Sybil attackers, market makers, and exchange hot wallets form distinct cluster types.",
|
|
),
|
|
]
|
|
|
|
# Expand with variations to reach calibration size
|
|
while len(calibration_pairs) < CALIBRATION_SIZE:
|
|
for q, d in calibration_pairs[:10]:
|
|
# Vary the query slightly
|
|
variations = [
|
|
(q + " in crypto", d),
|
|
(q + " explained", d),
|
|
("explain " + q, d),
|
|
("what is " + q, d),
|
|
]
|
|
calibration_pairs.extend(variations)
|
|
if len(calibration_pairs) >= CALIBRATION_SIZE:
|
|
break
|
|
|
|
class CalibrationDataReader:
|
|
def __init__(self, pairs, tokenizer, max_len):
|
|
self.pairs = pairs[:CALIBRATION_SIZE]
|
|
self.tokenizer = tokenizer
|
|
self.max_len = max_len
|
|
self.iter = iter(self._generate())
|
|
|
|
def _generate(self):
|
|
for query, doc in self.pairs:
|
|
encoded = self.tokenizer(
|
|
query,
|
|
doc,
|
|
return_tensors="np",
|
|
truncation=True,
|
|
max_length=self.max_len,
|
|
padding="max_length",
|
|
)
|
|
yield {
|
|
"input_ids": encoded["input_ids"],
|
|
"attention_mask": encoded["attention_mask"],
|
|
}
|
|
|
|
def get_next(self):
|
|
try:
|
|
return next(self.iter)
|
|
except StopIteration:
|
|
return None
|
|
|
|
return CalibrationDataReader(calibration_pairs, tokenizer, max_seq_len)
|
|
|
|
|
|
def main():
|
|
print(f"Static INT8 quantization for {MODEL_NAME}...")
|
|
start = time.time()
|
|
|
|
onnx_model_path = os.path.join(ONNX_DIR, "model.onnx")
|
|
if not os.path.exists(onnx_model_path):
|
|
print(f"ERROR: ONNX model not found at {onnx_model_path}")
|
|
print("Run quantize_reranker.py first to export to ONNX.")
|
|
return 1
|
|
|
|
# 1. Load tokenizer for calibration
|
|
print(" [1/3] Loading tokenizer...")
|
|
from transformers import AutoTokenizer
|
|
|
|
tokenizer = AutoTokenizer.from_pretrained("BAAI/bge-reranker-v2-m3")
|
|
print(" ✓ Tokenizer loaded")
|
|
|
|
# 2. Create calibration data
|
|
print(f" [2/3] Creating calibration dataset ({CALIBRATION_SIZE} samples)...")
|
|
calibration_reader = create_calibration_reader(tokenizer)
|
|
print(" ✓ Calibration data ready")
|
|
|
|
# 3. Static quantization
|
|
print(" [3/3] Running static INT8 quantization...")
|
|
from onnxruntime.quantization import QuantFormat, QuantType, quantize_static
|
|
|
|
quant_model_path = os.path.join(ONNX_DIR, "model_int8.onnx")
|
|
|
|
quantize_static(
|
|
model_input=onnx_model_path,
|
|
model_output=quant_model_path,
|
|
calibration_data_reader=calibration_reader,
|
|
quant_format=QuantFormat.QOperator,
|
|
weight_type=QuantType.QInt8,
|
|
activation_type=QuantType.QInt8,
|
|
per_channel=True,
|
|
reduce_range=True,
|
|
extra_options={"CalibMovingAverage": True, "SmoothQuant": False},
|
|
)
|
|
|
|
# Replace original ONNX with quantized version
|
|
os.rename(onnx_model_path, os.path.join(ONNX_DIR, "model_fp32.onnx"))
|
|
os.rename(quant_model_path, onnx_model_path)
|
|
|
|
# 4. Verify size
|
|
size_mb = sum(
|
|
os.path.getsize(os.path.join(dirpath, f)) for dirpath, _, filenames in os.walk(ONNX_DIR) for f in filenames
|
|
) / (1024 * 1024)
|
|
|
|
elapsed = time.time() - start
|
|
print(f" ✓ Done in {elapsed:.1f}s")
|
|
print(f" Quantized model: {size_mb:.0f}MB (fp32 original: ~2100MB)")
|
|
print(f" Saved to: {ONNX_DIR}/model.onnx")
|
|
|
|
if size_mb < 1000:
|
|
print(f"\n 🎉 SUCCESS: Model compressed {2100 / size_mb:.1f}x!")
|
|
else:
|
|
print(f"\n ⚠️ Model still large ({size_mb:.0f}MB).")
|
|
print(" ONNX static quantization for cross-encoders is limited.")
|
|
print(" The main win is inference speed (2-3x faster), not file size.")
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|