grabbou commited on
Commit
6490f58
·
verified ·
1 Parent(s): 9611076

Add the export script

Browse files
Files changed (1) hide show
  1. export.py +140 -0
export.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # /// script
2
+ # requires-python = ">=3.11"
3
+ # dependencies = [
4
+ # "coreai-core==1.0.0b3",
5
+ # "coreai-torch==0.4.3",
6
+ # "transformers==4.57.3",
7
+ # "torch<=2.13.0",
8
+ # ]
9
+ #
10
+ # [tool.uv]
11
+ # index-url = "https://pypi.org/simple"
12
+ # index-strategy = "unsafe-best-match"
13
+ # ///
14
+ """CLIP ViT-B/32 as one .aimodel with separate text and image encoders.
15
+
16
+ Apple's models/clip recipe traces one function that needs an image and a fixed
17
+ batch of 3 prompts on every call. Embedding text or images alone needs each
18
+ tower on its own, so this exports:
19
+
20
+ - encode_text(input_ids, attention_mask) -> text_embeds, inputs (B, 77) int32,
21
+ padded to CLIP's 77-token context
22
+ - encode_image(pixel_values) -> image_embeds, input (B, 3, 224, 224)
23
+
24
+ Both batch dims are dynamic (1-64), and the embeddings are L2-normalized.
25
+ """
26
+
27
+ import argparse
28
+ import shutil
29
+ import time
30
+ from pathlib import Path
31
+
32
+ import torch
33
+ import transformers
34
+ from coreai.runtime import AIModelAssetMetadata
35
+ from coreai_torch import TorchConverter, get_decomp_table
36
+
37
+ MODEL = "openai/clip-vit-base-patch32"
38
+
39
+
40
+ class TextEncoder(torch.nn.Module):
41
+ def __init__(self, model):
42
+ super().__init__()
43
+ self.text_model = model.text_model
44
+ self.text_projection = model.text_projection
45
+
46
+ def forward(self, input_ids, attention_mask):
47
+ pooled = self.text_model(input_ids=input_ids, attention_mask=attention_mask)[1]
48
+ embeds = self.text_projection(pooled)
49
+ return embeds / embeds.norm(p=2, dim=-1, keepdim=True)
50
+
51
+
52
+ class ImageEncoder(torch.nn.Module):
53
+ def __init__(self, model):
54
+ super().__init__()
55
+ self.vision_model = model.vision_model
56
+ self.visual_projection = model.visual_projection
57
+
58
+ def forward(self, pixel_values):
59
+ pooled = self.vision_model(pixel_values=pixel_values)[1]
60
+ embeds = self.visual_projection(pooled)
61
+ return embeds / embeds.norm(p=2, dim=-1, keepdim=True)
62
+
63
+
64
+ def export(module, kwargs, dynamic_shapes, dtype):
65
+ with torch.autocast(device_type="cpu", dtype=dtype):
66
+ exported = torch.export.export(module, args=(), kwargs=kwargs, dynamic_shapes=dynamic_shapes)
67
+ return exported.run_decompositions(get_decomp_table())
68
+
69
+
70
+ def main():
71
+ parser = argparse.ArgumentParser()
72
+ parser.add_argument("--output-dir", required=True)
73
+ parser.add_argument("--dtype", choices=["float16", "float32"], default="float16")
74
+ args = parser.parse_args()
75
+ dtype = getattr(torch, args.dtype)
76
+
77
+ model = transformers.CLIPModel.from_pretrained(MODEL).eval().to(dtype)
78
+ tokenizer = transformers.CLIPTokenizer.from_pretrained(MODEL)
79
+ text = tokenizer(
80
+ ["a photo of a cat", "a photo of a dog"],
81
+ return_tensors="pt",
82
+ padding="max_length",
83
+ max_length=77,
84
+ )
85
+ batch = torch.export.Dim("batch", min=1, max=64)
86
+
87
+ text_program = export(
88
+ TextEncoder(model),
89
+ {
90
+ "input_ids": text["input_ids"].to(torch.int32),
91
+ "attention_mask": text["attention_mask"].to(torch.int32),
92
+ },
93
+ {"input_ids": {0: batch}, "attention_mask": {0: batch}},
94
+ dtype,
95
+ )
96
+ image_program = export(
97
+ ImageEncoder(model),
98
+ {"pixel_values": torch.randn(2, 3, 224, 224).to(dtype)},
99
+ {"pixel_values": {0: batch}},
100
+ dtype,
101
+ )
102
+
103
+ converter = (
104
+ TorchConverter(mode=TorchConverter.Mode.RELEASE)
105
+ .add_exported_program(
106
+ exported_program=text_program,
107
+ input_names=["input_ids", "attention_mask"],
108
+ output_names=["text_embeds"],
109
+ entrypoint_name="encode_text",
110
+ )
111
+ .add_exported_program(
112
+ exported_program=image_program,
113
+ input_names=["pixel_values"],
114
+ output_names=["image_embeds"],
115
+ entrypoint_name="encode_image",
116
+ )
117
+ )
118
+ program = converter.to_coreai()
119
+
120
+ metadata = AIModelAssetMetadata()
121
+ metadata.author = "A. Radford et al."
122
+ metadata.license = "MIT"
123
+ metadata.model_description = (
124
+ "CLIP ViT-B/32 text and image encoders with L2-normalized embeddings. "
125
+ "Source: https://huggingface.co/openai/clip-vit-base-patch32"
126
+ )
127
+ metadata.creation_date = int(time.time())
128
+
129
+ path = Path(args.output_dir) / f"clip-vit-base-patch32-encoders_{args.dtype}.aimodel"
130
+ if path.exists():
131
+ shutil.rmtree(path)
132
+ path.parent.mkdir(parents=True, exist_ok=True)
133
+ program.save_asset(path, metadata)
134
+ # The tokenizer travels with the model, so the app needs nothing else
135
+ tokenizer.save_pretrained(path.parent / "clip-tokenizer")
136
+ print(f"[INFO] Saved {path}")
137
+
138
+
139
+ if __name__ == "__main__":
140
+ main()