barakplasma commited on
Commit
ac55bec
·
unverified ·
1 Parent(s): 4f5d51c

Upload scripts/bundle_litertlm.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. scripts/bundle_litertlm.py +218 -0
scripts/bundle_litertlm.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Bundle a Strategy-1 KV-cache TFLite + SentencePiece tokenizer into a
4
+ .litertlm file compatible with Google AI Edge / LiteRT-LM runtime.
5
+
6
+ Embeds:
7
+ - LlmMetadata proto: Gemma3 model type, 2K max tokens, TranslateGemma
8
+ Jinja chat template, BOS/EOS/end_of_turn stop tokens
9
+ - TFLite model (model_type=prefill_decode)
10
+ - SentencePiece tokenizer
11
+
12
+ Usage:
13
+ python bundle_litertlm.py \
14
+ --tflite /path/to/model.tflite \
15
+ --tokenizer /path/to/tokenizer.model \
16
+ --output /path/to/output.litertlm \
17
+ [--max-tokens 2048]
18
+ """
19
+
20
+ import argparse
21
+ import sys
22
+ import tempfile
23
+ from pathlib import Path
24
+
25
+ # Make litert_lm package importable from /tmp/litert-lm-pkg
26
+ sys.path.insert(0, "/tmp/litert-lm-pkg")
27
+
28
+ from litert_lm_builder import litertlm_builder
29
+ from litert_lm.runtime.proto import (
30
+ llm_metadata_pb2,
31
+ llm_model_type_pb2,
32
+ token_pb2,
33
+ )
34
+
35
+
36
+ # TranslateGemma 4B IT Jinja chat template (from tokenizer_config.json)
37
+ # Requires structured content: {type, source_lang_code, target_lang_code, text}
38
+ TRANSLATE_GEMMA_JINJA_TEMPLATE = """\
39
+ {%- set languages = {
40
+ "aa": "Afar", "ab": "Abkhazian", "af": "Afrikaans", "ak": "Akan",
41
+ "am": "Amharic", "an": "Aragonese", "ar": "Arabic", "as": "Assamese",
42
+ "az": "Azerbaijani", "ba": "Bashkir", "be": "Belarusian", "bg": "Bulgarian",
43
+ "bm": "Bambara", "bn": "Bengali", "bo": "Tibetan", "br": "Breton",
44
+ "bs": "Bosnian", "ca": "Catalan", "ce": "Chechen", "co": "Corsican",
45
+ "cs": "Czech", "cv": "Chuvash", "cy": "Welsh", "da": "Danish",
46
+ "de": "German", "dv": "Divehi", "dz": "Dzongkha", "ee": "Ewe",
47
+ "el": "Greek", "en": "English", "eo": "Esperanto", "es": "Spanish",
48
+ "et": "Estonian", "eu": "Basque", "fa": "Persian", "ff": "Fulah",
49
+ "fi": "Finnish", "fo": "Faroese", "fr": "French", "fy": "Western Frisian",
50
+ "ga": "Irish", "gd": "Scottish Gaelic", "gl": "Galician", "gn": "Guarani",
51
+ "gu": "Gujarati", "gv": "Manx", "ha": "Hausa", "he": "Hebrew",
52
+ "hi": "Hindi", "hr": "Croatian", "ht": "Haitian", "hu": "Hungarian",
53
+ "hy": "Armenian", "ia": "Interlingua", "id": "Indonesian", "ig": "Igbo",
54
+ "ii": "Sichuan Yi", "ik": "Inupiaq", "io": "Ido", "is": "Icelandic",
55
+ "it": "Italian", "iu": "Inuktitut", "ja": "Japanese", "jv": "Javanese",
56
+ "ka": "Georgian", "ki": "Kikuyu", "kk": "Kazakh", "kl": "Kalaallisut",
57
+ "km": "Central Khmer", "kn": "Kannada", "ko": "Korean", "ks": "Kashmiri",
58
+ "ku": "Kurdish", "kw": "Cornish", "ky": "Kyrgyz", "la": "Latin",
59
+ "lb": "Luxembourgish", "lg": "Ganda", "ln": "Lingala", "lo": "Lao",
60
+ "lt": "Lithuanian", "lu": "Luba-Katanga", "lv": "Latvian", "mg": "Malagasy",
61
+ "mi": "Maori", "mk": "Macedonian", "ml": "Malayalam", "mn": "Mongolian",
62
+ "mr": "Marathi", "ms": "Malay", "mt": "Maltese", "my": "Burmese",
63
+ "nb": "Norwegian Bokmål", "nd": "North Ndebele", "ne": "Nepali",
64
+ "nl": "Dutch", "nn": "Norwegian Nynorsk", "no": "Norwegian",
65
+ "nr": "South Ndebele", "nv": "Navajo", "ny": "Chichewa", "oc": "Occitan",
66
+ "om": "Oromo", "or": "Oriya", "os": "Ossetian", "pa": "Punjabi",
67
+ "pl": "Polish", "ps": "Pashto", "pt": "Portuguese", "qu": "Quechua",
68
+ "rm": "Romansh", "rn": "Rundi", "ro": "Romanian", "ru": "Russian",
69
+ "rw": "Kinyarwanda", "sa": "Sanskrit", "sc": "Sardinian", "sd": "Sindhi",
70
+ "se": "Northern Sami", "sg": "Sango", "si": "Sinhala", "sk": "Slovak",
71
+ "sl": "Slovenian", "sn": "Shona", "so": "Somali", "sq": "Albanian",
72
+ "sr": "Serbian", "ss": "Swati", "st": "Southern Sotho", "su": "Sundanese",
73
+ "sv": "Swedish", "sw": "Swahili", "ta": "Tamil", "te": "Telugu",
74
+ "tg": "Tajik", "th": "Thai", "ti": "Tigrinya", "tk": "Turkmen",
75
+ "tl": "Tagalog", "tn": "Tswana", "to": "Tonga", "tr": "Turkish",
76
+ "ts": "Tsonga", "tt": "Tatar", "ug": "Uyghur", "uk": "Ukrainian",
77
+ "ur": "Urdu", "uz": "Uzbek", "ve": "Venda", "vi": "Vietnamese",
78
+ "vo": "Volapük", "wa": "Walloon", "wo": "Wolof", "xh": "Xhosa",
79
+ "yi": "Yiddish", "yo": "Yoruba", "za": "Zhuang", "zh": "Chinese",
80
+ "zu": "Zulu"
81
+ } -%}
82
+ {{ bos_token }}
83
+ {%- if (messages[0]['role'] != 'user') -%}
84
+ {{ raise_exception("Conversations must start with a user prompt.") }}
85
+ {%- endif -%}
86
+ {%- for message in messages -%}
87
+ {%- if (message['role'] == 'user') != (loop.index0 % 2 == 0) -%}
88
+ {{ raise_exception("Conversation roles must alternate user/assistant/user/assistant/...") }}
89
+ {%- endif -%}
90
+ {%- if (message['role'] == 'assistant') -%}
91
+ {%- if message['content'] is none or message['content'] is not string -%}
92
+ {{ raise_exception("Assistant role must provide content as a string") }}
93
+ {%- endif -%}
94
+ {{ '<start_of_turn>model\\n'}}
95
+ {{ message["content"] | trim }}
96
+ {%- elif (message['role'] == 'user') -%}
97
+ {%- set content = message["content"] if message["content"] is string else message["content"][0] -%}
98
+ {%- if content is string -%}
99
+ {%- set source_lang = "English" -%}
100
+ {%- set source_lang_code = "en" -%}
101
+ {%- set target_lang = "Spanish" -%}
102
+ {%- set target_lang_code = "es" -%}
103
+ {{ '<start_of_turn>user\\nYou are a professional ' + source_lang + ' (' + source_lang_code + ') to ' +
104
+ target_lang + ' (' + target_lang_code + ') translator. Your goal is to accurately convey the meaning ' +
105
+ 'and nuances of the original ' + source_lang + ' text while adhering to ' + target_lang + ' grammar, ' +
106
+ 'vocabulary, and cultural sensitivities.\\n' +
107
+ 'Produce only the ' + target_lang + ' translation, without any additional explanations or ' +
108
+ 'commentary. Please translate the following ' + source_lang + ' text into ' + target_lang + ':\\n\\n\\n' +
109
+ content | trim
110
+ }}
111
+ {%- else -%}
112
+ {%- set source_lang_code = content["source_lang_code"] | replace("_", "-") -%}
113
+ {%- set source_lang = languages.get(source_lang_code, source_lang_code) -%}
114
+ {%- set target_lang_code = content["target_lang_code"] | replace("_", "-") -%}
115
+ {%- set target_lang = languages.get(target_lang_code, target_lang_code) -%}
116
+ {{ '<start_of_turn>user\\nYou are a professional ' + source_lang + ' (' + source_lang_code + ') to ' +
117
+ target_lang + ' (' + target_lang_code + ') translator. Your goal is to accurately convey the meaning ' +
118
+ 'and nuances of the original ' + source_lang + ' text while adhering to ' + target_lang + ' grammar, ' +
119
+ 'vocabulary, and cultural sensitivities.\\n'
120
+ }}
121
+ {%- if content["type"] == 'text' -%}
122
+ {{ 'Produce only the ' + target_lang + ' translation, without any additional explanations or ' +
123
+ 'commentary. Please translate the following ' + source_lang + ' text into ' + target_lang + ':\\n\\n\\n' +
124
+ content["text"] | trim
125
+ }}
126
+ {%- endif -%}
127
+ {%- endif -%}
128
+ {%- else -%}
129
+ {{ raise_exception("Conversations must only contain user or assistant roles.") }}
130
+ {%- endif -%}
131
+ {{ '<end_of_turn>\\n' }}
132
+ {%- endfor -%}
133
+ {%- if add_generation_prompt -%}
134
+ {{'<start_of_turn>model\\n'}}
135
+ {%- endif -%}"""
136
+
137
+
138
+ def build_llm_metadata_proto(max_tokens: int) -> bytes:
139
+ meta = llm_metadata_pb2.LlmMetadata()
140
+ meta.max_num_tokens = max_tokens
141
+
142
+ # Model type: Gemma3 (text-only variant — no vision config needed for TranslateGemma text mode)
143
+ meta.llm_model_type.gemma3.CopyFrom(llm_model_type_pb2.Gemma3())
144
+
145
+ # Start token: BOS = token id 2
146
+ meta.start_token.token_ids.ids.append(2)
147
+
148
+ # Stop tokens: EOS (id=1) and end_of_turn (id=106)
149
+ eos = meta.stop_tokens.add()
150
+ eos.token_ids.ids.append(1)
151
+
152
+ eot = meta.stop_tokens.add()
153
+ eot.token_ids.ids.append(106)
154
+
155
+ # Embed the Jinja template
156
+ meta.jinja_prompt_template = TRANSLATE_GEMMA_JINJA_TEMPLATE
157
+
158
+ return meta.SerializeToString()
159
+
160
+
161
+ def main():
162
+ ap = argparse.ArgumentParser(description="Bundle TFLite + tokenizer into .litertlm")
163
+ ap.add_argument("--tflite", required=True)
164
+ ap.add_argument("--tokenizer", required=True, help="SentencePiece .model file")
165
+ ap.add_argument("--output", required=True)
166
+ ap.add_argument("--max-tokens", type=int, default=2048)
167
+ ap.add_argument("--quant", default="int8", help="Quantization label for metadata")
168
+ args = ap.parse_args()
169
+
170
+ tflite_path = Path(args.tflite)
171
+ tokenizer_path = Path(args.tokenizer)
172
+ output_path = Path(args.output)
173
+
174
+ if not tflite_path.exists():
175
+ print(f"[x] TFLite not found: {tflite_path}", file=sys.stderr)
176
+ sys.exit(1)
177
+ if not tokenizer_path.exists():
178
+ print(f"[x] Tokenizer not found: {tokenizer_path}", file=sys.stderr)
179
+ sys.exit(1)
180
+
181
+ output_path.parent.mkdir(parents=True, exist_ok=True)
182
+
183
+ # Write LlmMetadata to temp file
184
+ meta_bytes = build_llm_metadata_proto(args.max_tokens)
185
+ with tempfile.NamedTemporaryFile(suffix=".pb", delete=False) as f:
186
+ meta_file = Path(f.name)
187
+ f.write(meta_bytes)
188
+
189
+ print(f"[+] Building .litertlm: {output_path.name}")
190
+ print(f" TFLite: {tflite_path} ({tflite_path.stat().st_size / 1e9:.2f} GB)")
191
+ print(f" Tokenizer: {tokenizer_path}")
192
+ print(f" Max tokens: {args.max_tokens}")
193
+
194
+ Metadata = litertlm_builder.Metadata
195
+ DType = litertlm_builder.DType
196
+
197
+ builder = litertlm_builder.LitertLmFileBuilder()
198
+ builder.add_system_metadata(Metadata(key="model_name", value=f"TranslateGemma-4B-IT-{args.quant}", dtype=DType.STRING))
199
+ builder.add_system_metadata(Metadata(key="authors", value="google", dtype=DType.STRING))
200
+ builder.add_system_metadata(Metadata(key="quantization", value=args.quant, dtype=DType.STRING))
201
+
202
+ builder.add_tflite_model(
203
+ str(tflite_path),
204
+ model_type=litertlm_builder.TfLiteModelType.PREFILL_DECODE,
205
+ )
206
+ builder.add_sentencepiece_tokenizer(str(tokenizer_path))
207
+ builder.add_llm_metadata(str(meta_file))
208
+
209
+ with open(output_path, "wb") as f:
210
+ builder.build(f)
211
+ meta_file.unlink(missing_ok=True)
212
+
213
+ size = output_path.stat().st_size
214
+ print(f"[+] Written: {output_path} ({size / 1e9:.2f} GB)")
215
+
216
+
217
+ if __name__ == "__main__":
218
+ main()