dizyann's picture
feat: support the full Crivo model family
6986878
Raw History Blame Contribute Delete
6.9 kB
"""Reproducible full-graph Crivo export. Conversion-only Python; app runtime stays TypeScript.
Run with the pinned environment in requirements.txt. Source snapshots must match sources.json.
"""
import argparse, gc, importlib.util, json, shutil, sys
from pathlib import Path
import numpy as np
import onnx
import onnxruntime as ort
from quantize import quantize_weights
import torch
from transformers import AutoTokenizer, ModernBertConfig, ModernBertModel
from safetensors.torch import load_file
parser = argparse.ArgumentParser()
parser.add_argument('size', choices=['17m','32m','68m','150m','400m','1b'])
args = parser.parse_args()
root = Path(__file__).resolve().parents[2]
source = root / '.cache/conversion' / args.size
out = root / '.cache/variants' / args.size
out.mkdir(parents=True, exist_ok=True)
(out/'onnx').mkdir(exist_ok=True)
meta = json.loads((source/'source.json').read_text())
assert meta == json.loads(Path(__file__).with_name('sources.json').read_text())[args.size], 'Source revision mismatch'
torch.set_num_threads(4)
spec = importlib.util.spec_from_file_location('upstream_model', source/'model.py')
upstream = importlib.util.module_from_spec(spec)
spec.loader.exec_module(upstream)
original = json.loads((source/'config.json').read_text())
config = ModernBertConfig(**{k:v for k,v in original.items() if k not in ('architectures','decision')})
config._attn_implementation = 'eager'
config.reference_compile = False
model = upstream.DecisionModel(ModernBertModel(config), config.hidden_size).eval()
model.load_state_dict(load_file(str(source/'model.safetensors')), strict=True)
tok = AutoTokenizer.from_pretrained(source)
tok.save_pretrained(out)
tc = json.loads((out/'tokenizer_config.json').read_text())
tc['tokenizer_class'] = 'PreTrainedTokenizer'
(out/'tokenizer_config.json').write_text(json.dumps(tc,indent=2)+'\n')
exported = {**original, 'model_type':'custom','architectures':['PreTrainedModel'], 'source':meta}
# Browser release initially retains the tested 7,999-token cap, not upstream's 32k claim.
exported['web_max_length'] = 7999
names = ['input_ids','attention_mask','opt_positions','opt_mask','qtype']
encoder = upstream.Encoder(tok)
cases = [
('Please refund my duplicate payment.', [('choice','Identify the intent.',{'refund':'Return money','cancel':'Cancel order','other':'Another request'}),('noul','The customer was charged twice.',None),('score','Rate urgency.',['Low','Medium','High'])]),
('The order is fine, I only want to know the delivery date.', [('choice','Identify intent.',{'question':'Ask a question','refund':'Return money'} )]),
('A long support request. '*30,[('score','Rate urgency.',['Low','High']),('noul','The service is unavailable.',{'true':'Unavailable','false':'Available'})])
]
def inputs(state, questions):
rows=[]
for kind,instructions,criteria in questions:
options,_=upstream.option_texts(kind,criteria,instructions)
ids,pos=encoder.encode(instructions,state,options)
rows.append((ids,pos,upstream.QUESTION_TYPES[kind]))
return upstream.collate(rows,tok.pad_token_id)
samples=[inputs(*case) for case in cases]
references=[]
with torch.inference_mode():
for feeds in samples: references.append(model(**feeds).numpy())
graph=out/'onnx/model.onnx'
print('EXPORT',args.size,flush=True)
with torch.no_grad():
torch.onnx.export(model,tuple(samples[0][name] for name in names),str(graph),input_names=names,output_names=['logits'],opset_version=17,dynamo=False,external_data=True,dynamic_axes={**{n:{0:'batch',1:'sequence'} for n in names[:2]},**{n:{0:'batch',1:'options'} for n in names[2:4]},'qtype':{0:'batch'},'logits':{0:'batch',1:'options'}})
del model;gc.collect()
# Keep each external buffer below browser ArrayBuffer limits.
def shard_external(path):
g=onnx.load(str(path))
index=0;offset=0;handle=None
try:
for tensor in g.graph.initializer:
if not tensor.HasField('raw_data'): continue
data=tensor.raw_data
if handle is None or offset+len(data)>1024**3:
if handle: handle.close();index+=1
location=path.name+'_data'+('' if index==0 else '_'+str(index))
handle=(path.parent/location).open('wb');offset=0
onnx.external_data_helper.set_external_data(tensor,location,offset,len(data))
handle.write(data);offset+=len(data)
tensor.ClearField('raw_data');tensor.data_location=onnx.TensorProto.EXTERNAL
finally:
if handle: handle.close()
onnx.save_model(g,str(path))
del g;gc.collect()
return index+1
if args.size=='1b':
exported['transformers.js_config']={'use_external_data_format':{'model.onnx':shard_external(graph)}}
(out/'config.json').write_text(json.dumps(exported,indent=2)+'\n')
onnx.checker.check_model(str(graph))
opts=ort.SessionOptions();opts.intra_op_num_threads=4
session=ort.InferenceSession(str(graph),sess_options=opts,providers=['CPUExecutionProvider'])
fp_delta=0
for feeds,expected in zip(samples,references):
actual=session.run(['logits'],{n:feeds[n].numpy() for n in names})[0]
np.testing.assert_allclose(actual,expected,rtol=1e-3,atol=1e-4)
fp_delta=max(fp_delta,float(np.max(np.abs(actual-expected))))
del session;gc.collect()
print('QUANTIZE',args.size,flush=True)
quant=out/'onnx/model_quantized.onnx'
quantize_weights(graph, quant)
if args.size=='1b':
exported['transformers.js_config']['use_external_data_format']['model_quantized.onnx']=shard_external(quant)
(out/'config.json').write_text(json.dumps(exported,indent=2)+'\n')
onnx.checker.check_model(str(quant))
session=ort.InferenceSession(str(quant),sess_options=opts,providers=['CPUExecutionProvider'])
quant_delta=0
fixtures=[]
for (state,questions),feeds,expected in zip(cases,samples,references):
actual=session.run(['logits'],{n:feeds[n].numpy() for n in names})[0]
assert np.isfinite(actual).all()
p=torch.from_numpy(expected).double().softmax(-1).numpy()
q=torch.from_numpy(actual).double().softmax(-1).numpy()
quant_delta=max(quant_delta,float(np.max(np.abs(p-q))))
fixtures.append({'state':state,'questions':[{'type':k,'instructions':i,**({'criteria':c} if c is not None else {})} for k,i,c in questions],'fp32':p.tolist(),'int8':q.tolist()})
report={'quantization':{'storage':'int8','arithmetic':'fp32','scheme':'symmetric-per-channel-weight-only'},'source':meta,'fp32_max_logit_error':fp_delta,'int8_max_probability_error':quant_delta,'cases':fixtures,'versions':{'torch':torch.__version__,'onnx':onnx.__version__,'onnxruntime':ort.__version__}}
# Retain only the final graphs and referenced external buffers.
keep = {'model.onnx', 'model_quantized.onnx'}
for path in [graph, quant]:
g=onnx.load(str(path),load_external_data=False)
for tensor in g.graph.initializer:
keep.update(item.value for item in tensor.external_data if item.key=='location')
for path in (out/'onnx').iterdir():
if path.is_file() and path.name not in keep: path.unlink()
(out/'parity.json').write_text(json.dumps(report,indent=2)+'\n')
print('PASS',args.size,fp_delta,quant_delta,flush=True)