sys1mlx / experimental /ane_export.py
ardanila's picture
Publish Kev Q4 MLX decision model with experimental vision and portable runtime
0daf1e7 verified
Raw History Blame Contribute Delete
5.38 kB
"""Export the existing Kev vision tower directly to Core ML MIL, fixed grid.
Uses the fixed-grid/full-image-attention design in john-rocky/CoreML-LLM,
but reads our verified MLX weights and needs no PyTorch or model download.
No resampling or token reduction is performed. FP16 arithmetic differs from
the original BF16 tower and is explicitly evaluated, not assumed equivalent.
"""
import argparse,hashlib,json,time
from pathlib import Path
import numpy as np
import mlx.core as mx
from mlx.utils import tree_flatten
from mlx_vlm.models.qwen3_vl.vision import VisionModel
from mlx_vlm.models.qwen3_vl.config import VisionConfig
import coremltools as ct
from coremltools.converters.mil import Builder as mb
from coremltools.converters.mil.mil import types
ROOT=Path(__file__).resolve().parents[1];OUT=ROOT/'coreml_exports'
def main():
p=argparse.ArgumentParser();p.add_argument('--grid',type=int,default=24)
p.add_argument('--attention',choices=['manual','sdpa'],default='manual');p.add_argument('--out');args=p.parse_args()
dest=OUT/(args.out or f'grid-{args.grid}-{args.attention}');dest.mkdir(parents=True,exist_ok=False)
config=json.loads((ROOT/'config.json').read_text())['vision_config']
assert not config['deepstack_visual_indexes']
checkpoint=ROOT/'model.safetensors'
h=hashlib.sha256()
with checkpoint.open('rb') as f:
for b in iter(lambda:f.read(8*1024**2),b''):h.update(b)
expected=json.loads((ROOT/'provenance.json').read_text())['sha256'];assert h.hexdigest()==expected
model=VisionModel(VisionConfig.from_dict(config))
loaded=mx.load(str(checkpoint));model.load_weights([(k.removeprefix('vision_tower.'),v) for k,v in loaded.items() if k.startswith('vision_tower.')],strict=True)
del loaded;model.eval();mx.eval(model.parameters());mx.clear_cache()
weights={k:np.array(v.astype(mx.float32)).astype(np.float16) for k,v in tree_flatten(model.parameters())}
grid=mx.array([[1,args.grid,args.grid]],dtype=mx.int32);n=args.grid**2;d=config['hidden_size'];heads=config['num_heads'];hd=d//heads
pos=np.array(model.fast_pos_embed_interpolate(grid).astype(mx.float32)).astype(np.float16)
freqs=model.rot_pos_emb(grid);cos=np.array(mx.tile(mx.cos(freqs),(1,2))).astype(np.float16)[:,None,:]
sin=np.array(mx.tile(mx.sin(freqs),(1,2))).astype(np.float16)[:,None,:]
del model;mx.clear_cache()
(dest/'manifest.json').write_text(json.dumps({'checkpoint_sha256':expected,'grid':[1,args.grid,args.grid],
'image_tokens':n//4,'config':config,'coremltools':ct.__version__,'attention':args.attention,
'precision':'FP16 weights converted from original BF16; FP16 Core ML computation',
'script_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest()},indent=2)+'\n')
(dest/'ane_export.py').write_bytes(Path(__file__).read_bytes())
def linear(x,name):return mb.linear(x=x,weight=weights[name+'.weight'],bias=weights[name+'.bias'],name=name.replace('.','_'))
def norm(x,name):return mb.layer_norm(x=x,axes=[-1],gamma=weights[name+'.weight'],beta=weights[name+'.bias'],epsilon=np.float16(1e-6))
def rope(x):
a,b=mb.split(x=x,num_splits=2,axis=-1)
rotated=mb.concat(values=[mb.mul(x=b,y=np.float16(-1)),a],axis=-1)
return mb.add(x=mb.mul(x=x,y=cos),y=mb.mul(x=rotated,y=sin))
@mb.program(input_specs=[mb.TensorSpec(shape=(n,1536),dtype=types.fp16)],opset_version=ct.target.iOS18)
def program(pixels):
# MLX Conv3d stores [out,time,height,width,channel]; input patches are CTHW.
w=weights['patch_embed.proj.weight'].transpose(0,4,1,2,3).reshape(d,1536)
x=mb.linear(x=pixels,weight=w,bias=weights['patch_embed.proj.bias'])
x=mb.add(x=x,y=pos)
for i in range(config['depth']):
prefix=f'blocks.{i}';a=norm(x,prefix+'.norm1');qkv=linear(a,prefix+'.attn.qkv')
qkv=mb.reshape(x=qkv,shape=[n,3,heads,hd]);q,k,v=mb.split(x=qkv,num_splits=3,axis=1)
q=rope(mb.squeeze(x=q,axes=[1]));k=rope(mb.squeeze(x=k,axes=[1]));v=mb.squeeze(x=v,axes=[1])
q,k,v=[mb.expand_dims(x=mb.transpose(x=t,perm=[1,0,2]),axes=[0]) for t in [q,k,v]]
if args.attention=='sdpa':a=mb.scaled_dot_product_attention(query=q,key=k,value=v)
else:
scores=mb.matmul(x=mb.mul(x=q,y=np.float16(hd**-.5)),y=k,transpose_y=True)
a=mb.matmul(x=mb.softmax(x=scores,axis=-1),y=v)
a=mb.reshape(x=mb.transpose(x=a,perm=[0,2,1,3]),shape=[n,d])
x=mb.add(x=x,y=linear(a,prefix+'.attn.proj'))
a=norm(x,prefix+'.norm2');a=mb.gelu(x=linear(a,prefix+'.mlp.linear_fc1'),mode='TANH_APPROXIMATION')
x=mb.add(x=x,y=linear(a,prefix+'.mlp.linear_fc2'))
x=mb.reshape(x=norm(x,'merger.norm'),shape=[n//4,4*d])
x=mb.gelu(x=linear(x,'merger.linear_fc1'),mode='EXACT')
return mb.identity(x=linear(x,'merger.linear_fc2'),name='features')
print('MIL built',flush=True);t=time.perf_counter()
converted=ct.convert(program,convert_to='mlprogram',minimum_deployment_target=ct.target.iOS18,
compute_precision=ct.precision.FLOAT16,compute_units=ct.ComputeUnit.CPU_AND_NE,skip_model_load=True)
converted.save(str(dest/'vision.mlpackage'))
print(json.dumps({'saved':str(dest),'conversion_seconds':time.perf_counter()-t}),flush=True)
if __name__=='__main__':main()