yongqiang commited on
Commit
3a6e68f
·
1 Parent(s): 03f32f8

Add packaged audio inference support and refresh deployment assets

Browse files
.gitattributes CHANGED
@@ -41,5 +41,7 @@ main_axcl_x86 filter=lfs diff=lfs merge=lfs -text
41
  *.png filter=lfs diff=lfs merge=lfs -text
42
  *.jpg filter=lfs diff=lfs merge=lfs -text
43
  *.mp4 filter=lfs diff=lfs merge=lfs -text
 
 
44
  tokenizer.json filter=lfs diff=lfs merge=lfs -text
45
  gemma4_tokenizer.txt filter=lfs diff=lfs merge=lfs -text
 
41
  *.png filter=lfs diff=lfs merge=lfs -text
42
  *.jpg filter=lfs diff=lfs merge=lfs -text
43
  *.mp4 filter=lfs diff=lfs merge=lfs -text
44
+ *.mp3 filter=lfs diff=lfs merge=lfs -text
45
+ *.wav filter=lfs diff=lfs merge=lfs -text
46
  tokenizer.json filter=lfs diff=lfs merge=lfs -text
47
  gemma4_tokenizer.txt filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -6,6 +6,7 @@ base_model:
6
  tags:
7
  - gemma4
8
  - multimodal
 
9
  - axera
10
  - ax650
11
  language:
@@ -23,8 +24,8 @@ Ready-to-run deployment package for `google/gemma-4-E2B-it` on AX650 / NPU3.
23
  - This release packages the `w8a16` AXERA NPU runtime.
24
  - Compatible with `Pulsar2 5.2` and later.
25
  - Includes the tokenizer/config files required at runtime.
26
- - Includes compiled Gemma 4 text `.axmodel` files and Vision `.axmodel` files.
27
- - Supports both text-only chat and single-image multimodal inference.
28
 
29
  ## Supported Platform
30
 
@@ -42,8 +43,8 @@ This package has been validated on the following AX650-based devices:
42
 
43
  All measurements below were taken on AX650 / NPU3. `TTFT` stands for time to first token.
44
 
45
- - `w8a16`: TTFT is approximately `1664 ms`, with a decode throughput of approximately `10.44 tokens/s`.
46
- - `w4a16`: TTFT is approximately `1233.7 ms`, with a decode throughput of approximately `15.22 tokens/s`.
47
 
48
  The packaged text runtime in this release is the `w8a16` build. Its text runtime files are packaged at the repository root. The `w4a16` numbers are provided for reference only.
49
 
@@ -55,6 +56,22 @@ The packaged text runtime in this release is the `w8a16` build. Its text runtime
55
  | `gemma4_vision_h480_w672_t140.axmodel` | 480×672 | 140 | 258.329 ms |
56
  | `gemma4_vision_h672_w960_t280.axmodel` | 672×960 | 280 | 750.429 ms |
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  ## Package Layout
59
 
60
  ```text
@@ -69,6 +86,8 @@ The packaged text runtime in this release is the `w8a16` build. Its text runtime
69
  ├── gemma4_tokenizer.txt
70
  ├── gemma4_text_p128_l*.axmodel
71
  ├── gemma4_text_post.axmodel
 
 
72
  ├── gemma4_vision_h336_w480_t70.axmodel
73
  ├── gemma4_vision_h480_w672_t140.axmodel
74
  ├── gemma4_vision_h672_w960_t280.axmodel
@@ -80,7 +99,7 @@ The packaged text runtime in this release is the `w8a16` build. Its text runtime
80
  └── utils/
81
  ```
82
 
83
- This package uses a hybrid layout: the tokenizer stays in a subdirectory, the packaged text runtime files and Vision `.axmodel` files live at the repository root, and `vit_models/` keeps the accompanying Vision metadata JSON files.
84
 
85
  The Python demo scripts auto-detect the packaged paths above. If you keep this layout unchanged, you can run the Python examples later in this README without passing extra path arguments.
86
 
@@ -91,6 +110,14 @@ Both the `axllm` flow and the legacy Python demo flow below can use the packaged
91
 
92
  ![sample](assets/sample.png)
93
 
 
 
 
 
 
 
 
 
94
  ## Direct Inference with `axllm`
95
 
96
  > The `axllm` workflow is still being refined. The instructions below reflect the current validated flow and may be adjusted as the packaging continues to evolve.
@@ -144,91 +171,103 @@ For multimodal testing, you can use the sample image shown above: `./assets/samp
144
  $ axllm run AXERA-TECH/gemma-4-E2B-it
145
 
146
  # output log example:
147
- 15:04:24.522 INF Init:890 | LLM init start
148
- 15:04:24.522 INF Init:905 | shared kv enabled: num_kv_shared_layers=20
 
149
  tokenizer_type = 3
150
  huggingface tokenizer mode = space_replace_bpe
151
- 31% | ########## | 12 / 38 [4.47s<14.16s, 2.68 count/s] init 10 axmodel ok,remain_cmm(6047 MB 34% | ########## | 13 / 38 [4.61s<13.48s, 2.82 count/s] init 11 axmodel ok,remain_cmm(5992 MB 36% | ########### | 14 / 38 [4.78s<12.98s, 2.93 count/s] init 12 axmodel ok,remain_cmm(5937 MB 39% | ############ | 15 / 38 [4.93s<12.49s, 3.04 count/s] init 13 axmodel ok,remain_cmm(5882 MB 42% | ############# | 16 / 38 [5.09s<12.09s, 3.14 count/s] init 14 axmodel ok,remain_cmm(5813 MB 44% | ############## | 17 / 38 [5.28s<11.80s, 3.22 count/s] init 15 axmodel ok,remain_cmm(5727 MB 47% | ############### | 18 / 38 [5.50s<11.61s, 3.27 count/s] init 16 axmodel ok,remain_cmm(5642 MB 50% | ################ | 19 / 38 [5.69s<11.38s, 3.34 count/s] init 17 axmodel ok,remain_cmm(5557 MB 52% | ################ | 20 / 38 [5.91s<11.22s, 3.39 count/s] init 18 axmodel ok,remain_cmm(5471 MB 55% | ################# | 21 / 38 [6.11s<11.06s, 3.44 count/s] init 19 axmodel ok,remain_cmm(5373 MB 57% | ################## | 22 / 38 [6.31s<10.89s, 3.49 count/s] init 20 axmodel ok,remain_cmm(5287 MB 60% | ################### | 23 / 38 [6.53s<10.79s, 3.52 count/s] init 21 axmodel ok,remain_cmm(5202 MB 63% | #################### | 24 / 38 [6.75s<10.69s, 3.56 count/s] init 22 axmodel ok,remain_cmm(5117 MB 65% | ##################### | 25 / 38 [6.96s<10.58s, 3.59 count/s] init 23 axmodel ok,remain_cmm(5031 MB 68% | ##################### | 26 / 38 [7.18s<10.50s, 3.62 count/s] init 24 axmodel ok,remain_cmm(4933 MB 71% | ###################### | 27 / 38 [7.40s<10.41s, 3.65 count/s] init 25 axmodel ok,remain_cmm(4847 MB 73% | ####################### | 28 / 38 [7.62s<10.34s, 3.67 count/s] init 26 axmodel ok,remain_cmm(4762 MB 76% | ######################## | 29 / 38 [7.85s<10.28s, 3.70 count/s] init 27 axmodel ok,remain_cmm(4676 MB 78% | ######################### | 30 / 38 [8.13s<10.29s, 3.69 count/s] init 28 axmodel ok,remain_cmm(4591 MB 81% | ########################## | 31 / 38 [8.36s<10.25s, 3.71 count/s] init 29 axmodel ok,remain_cmm(4492 MB 84% | ########################## | 32 / 38 [8.60s<10.21s, 3.72 count/s] init 30 axmodel ok,remain_cmm(4407 MB 86% | ########################### | 33 / 38 [8.86s<10.21s, 3.72 count/s] init 31 axmodel ok,remain_cmm(4322 MB 89% | ############################ | 34 / 38 [9.11s<10.18s, 3.73 count/s] init 32 axmodel ok,remain_cmm(4236 MB 92% | ############################# | 35 / 38 [9.36s<10.16s, 3.74 count/s] init 33 axmodel ok,remain_cmm(4151 MB 94% | ############################## | 36 / 38 [9.62s<10.16s, 3.74 count/s] init 34 axmodel ok,remain_cmm(4052 MB 97% | ############################### | 37 / 38 [10.03s<10.30s, 3.69 count/s] init post axmodel ok,remain_cmm(3632 MB)
152
- 15:04:34.551 INF Init:1045 | max_token_len : 2047
153
- 15:04:34.551 INF Init:1048 | kv_cache_size : 256, kv_cache_num: 2047
154
- 15:04:34.551 INF init_groups_from_model:606 | prefill_token_num : 128
155
- 15:04:34.551 INF init_groups_from_model:820 | decode grp: 0, gid: 0, max_token_len : 2047
156
- 15:04:34.551 INF init_groups_from_model:824 | prefill grp: 0, gid: 1, history_cap: 0, total_cap: 128, symbolic_cap: 1
157
- 15:04:34.551 INF init_groups_from_model:824 | prefill grp: 1, gid: 2, history_cap: 128, total_cap: 256, symbolic_cap: 128
158
- 15:04:34.551 INF init_groups_from_model:824 | prefill grp: 2, gid: 3, history_cap: 256, total_cap: 384, symbolic_cap: 256
159
- 15:04:34.551 INF init_groups_from_model:824 | prefill grp: 3, gid: 4, history_cap: 384, total_cap: 512, symbolic_cap: 384
160
- 15:04:34.551 INF init_groups_from_model:824 | prefill grp: 4, gid: 5, history_cap: 512, total_cap: 640, symbolic_cap: 512
161
- 15:04:34.551 INF init_groups_from_model:824 | prefill grp: 5, gid: 6, history_cap: 640, total_cap: 768, symbolic_cap: 640
162
- 15:04:34.551 INF init_groups_from_model:824 | prefill grp: 6, gid: 7, history_cap: 768, total_cap: 896, symbolic_cap: 768
163
- 15:04:34.551 INF init_groups_from_model:824 | prefill grp: 7, gid: 8, history_cap: 896, total_cap: 1024, symbolic_cap: 896
164
- 15:04:34.551 INF init_groups_from_model:824 | prefill grp: 8, gid: 9, history_cap: 1024, total_cap: 1152, symbolic_cap: 1024
165
- 15:04:34.551 INF init_groups_from_model:831 | prefill_max_token_num : 1152
166
- 15:04:34.551 INF Init:27 | LLaMaEmbedSelector use mmap
167
- 100% | ################################ | 38 / 38 [10.03s<10.03s, 3.79 count/s] embed_selector init ok
168
- 15:04:34.567 INF Init:475 | Gemma4 per-layer helper enabled: vocab=262144 hidden=1536 layers=35 per_layer=256 pad=0
169
- 15:04:34.727 INF Init:785 | Gemma4-VL token ids: image_pad=258880 video_pad=258884
170
- 15:04:34.727 INF Init:792 | VisionModule init ok: type=Gemma4VL, tokens_per_block=70, embed_size=1536, out_dtype=fp32
171
- 15:04:34.727 WRN Init:801 | Vision preprocess backend: SimpleCV (OpenCV not found at build time; minor differences vs OpenCV are possible)
172
- 15:04:34.729 INF load_config:282 | load config:
173
- 15:04:34.729 INF load_config:282 | {
174
- 15:04:34.729 INF load_config:282 | "enable_repetition_penalty": false,
175
- 15:04:34.729 INF load_config:282 | "enable_temperature": true,
176
- 15:04:34.729 INF load_config:282 | "enable_top_k_sampling": false,
177
- 15:04:34.729 INF load_config:282 | "enable_top_p_sampling": true,
178
- 15:04:34.729 INF load_config:282 | "penalty_window": 64,
179
- 15:04:34.729 INF load_config:282 | "repetition_penalty": 1.0,
180
- 15:04:34.729 INF load_config:282 | "temperature": 1.0,
181
- 15:04:34.729 INF load_config:282 | "top_k": 64,
182
- 15:04:34.729 INF load_config:282 | "top_p": 0.95
183
- 15:04:34.729 INF load_config:282 | }
184
- 15:04:34.729 INF Init:1139 | LLM init ok
 
 
185
  Commands:
186
  /q, /exit 退出
187
  /reset 重置 kvcache
188
  /dd 删除一轮对话
189
  /pp 打印历史对话
190
  Ctrl+C: 停止当前生成
191
- VLM enabled: after each prompt, input media path (empty = text-only). Use "video:<frames_dir>" for video, "audio:<file>" for reserved audio placeholder.
192
  ----------------------------------------
193
- prompt >> who are you?
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  media >>
195
- 15:04:39.368 INF SetKVCache:1437 | decode_grpid:0 prefill_grpid:1 history_cap:0 total_cap:128 symbolic_cap:1 precompute_len:0 input_num_token:24 prefer_symbolic_group:0
196
- 15:04:39.368 INF SetKVCache:1458 | current prefill_max_token_num:1152
197
- 15:04:39.408 INF SetKVCache:1462 | first run
198
- 15:04:39.409 INF Run:1553 | input token num : 24, prefill_split_num : 1
199
- 15:04:39.482 INF Run:1640 | prefill chunk p=0 history_len=0 grpid=1 kv_cache_num=0 input_tokens=24
200
- 15:04:39.483 INF Run:1665 | prefill indices shape: p=0 idx_elems=128 idx_rows=1 pos_rows=0
201
- 15:04:39.764 INF Run:1837 | ttft: 355.37 ms
202
- I am Gemma 4, a Large Language Model developed by Google DeepMind. I am an open weights model.
203
-
204
- 15:04:44.087 NTC Run:2103 | hit eos,decode avg 5.09 token/s
205
- 15:04:44.088 INF GetKVCache:1408 | precompute_len:47, remaining:1105
206
- prompt >> Please describe the image in detail.
207
- media >> /root/yongqiang/auto_model_deployment/gemma-4-E2B-it/assets/sample.png
208
- 15:06:14.416 INF EncodeForContent:1122 | vision cache hit (disk): /root/yongqiang/auto_model_deployment/gemma-4-E2B-it/assets/sample.png
209
- 15:06:14.416 INF EncodeForContent:1131 | vision cache hit (mem): /root/yongqiang/auto_model_deployment/gemma-4-E2B-it/assets/sample.png
210
- 15:06:14.419 INF SetKVCache:1437 | decode_grpid:0 prefill_grpid:3 history_cap:256 total_cap:384 symbolic_cap:256 precompute_len:47 input_num_token:94 prefer_symbolic_group:1
211
- 15:06:14.419 INF SetKVCache:1458 | current prefill_max_token_num:1024
212
- 15:06:14.429 INF Run:1553 | input token num : 94, prefill_split_num : 1
213
- 15:06:14.703 INF Run:1640 | prefill chunk p=0 history_len=47 grpid=3 kv_cache_num=256 input_tokens=94
214
- 15:06:14.703 INF Run:1665 | prefill indices shape: p=0 idx_elems=128 idx_rows=1 pos_rows=0
215
- 15:06:15.027 INF Run:1837 | ttft: 597.95 ms
216
- I see an image of a cartoon character that resembles a cooked or stylized lobster.
217
-
218
- Here is a detailed description of the image:
219
-
220
- * **Subject:** The central subject is a bright red, stylized lobster.
221
- * **Style:** The illustration is highly cartoonish and vibrant, featuring thick outlines and bright, saturated colors, suggesting a playful or energetic style.
222
- * **Features:**
223
- * The lobster has large, expressive eyes and a wide, toothy grin, giving it a mischievous or energetic expression.
224
- * Its claws (pincers) are prominent and stylized.
225
- * The body is segmented, typical of a lobster, but rendered in a simplified, bold manner.
226
- * It has a curved, slightly exaggerated posture.
227
- * **Outline/Background:** The character is set against a plain white background. The image has a glossy or sticker-like finish, indicated by a slight shadow effect or outline around the character, suggesting it might be a graphic or sticker design.
228
- * **Overall Impression:** The image is energetic, bold, and fun, clearly designed as a mascot or a character illustration.
229
-
230
- 15:07:09.593 NTC Run:2103 | hit eos,decode avg 4.33 token/s
231
- 15:07:09.593 INF GetKVCache:1408 | precompute_len:378, remaining:774
232
  ```
233
 
234
 
@@ -241,44 +280,47 @@ Note: the command below assumes you run it from the parent directory of `AXERA-T
241
  ```bash
242
  $ axllm serve AXERA-TECH/gemma-4-E2B-it --port 8000
243
  # output log example:
244
- 16:22:21.336 INF Init:890 | LLM init start
245
- 16:22:21.336 INF Init:905 | shared kv enabled: num_kv_shared_layers=20
 
246
  tokenizer_type = 3
247
  huggingface tokenizer mode = space_replace_bpe
248
- 13% | #### | 5 / 38 [10.08s<76.64s, 0.50 count/s] init 3 axmodel ok,remain_cmm(4704 MB 15% | ##### | 6 / 38 [12.32s<78.01s, 0.49 count/s] init 4 axmodel ok,remain_cmm(4635 MB 18% | ##### | 7 / 38 [17.98s<97.59s, 0.39 count/s] init 5 axmodel ok,remain_cmm(4580 MB 21% | ###### | 8 / 38 [18.54s<88.05s, 0.43 count/s] init 6 axmodel ok,remain_cmm(4525 MB 23% | ####### | 9 / 38 [19.06s<80.49s, 0.47 count/s] init 7 axmodel ok,remain_cmm(4470 MB 26% | ######## | 10 / 38 [19.66s<74.70s, 0.51 count/s] init 8 axmodel ok,remain_cmm(4415 MB 28% | ######### | 11 / 38 [20.41s<70.49s, 0.54 count/s] init 9 axmodel ok,remain_cmm(4346 MB 31% | ########## | 12 / 38 [20.81s<65.91s, 0.58 count/s] init 10 axmodel ok,remain_cmm(4291 M 34% | ########## | 13 / 38 [21.22s<62.04s, 0.61 count/s] init 11 axmodel ok,remain_cmm(4236 M 36% | ########### | 14 / 38 [21.89s<59.42s, 0.64 count/s] init 12 axmodel ok,remain_cmm(4182 M 39% | ############ | 15 / 38 [22.24s<56.34s, 0.67 count/s] init 13 axmodel ok,remain_cmm(4127 M 42% | ############# | 16 / 38 [22.56s<53.57s, 0.71 count/s] init 14 axmodel ok,remain_cmm(4057 M 44% | ############## | 17 / 38 [23.11s<51.66s, 0.74 count/s] init 15 axmodel ok,remain_cmm(3972 M 47% | ############### | 18 / 38 [23.54s<49.69s, 0.76 count/s] init 16 axmodel ok,remain_cmm(3887 M 50% | ################ | 19 / 38 [24.28s<48.56s, 0.78 count/s] init 17 axmodel ok,remain_cmm(3801 M 52% | ################ | 20 / 38 [24.63s<46.80s, 0.81 count/s] init 18 axmodel ok,remain_cmm(3716 M 55% | ################# | 21 / 38 [24.92s<45.08s, 0.84 count/s] init 19 axmodel ok,remain_cmm(3617 M 57% | ################## | 22 / 38 [25.67s<44.33s, 0.86 count/s] init 20 axmodel ok,remain_cmm(3532 M 60% | ################### | 23 / 38 [26.33s<43.50s, 0.87 count/s] init 21 axmodel ok,remain_cmm(3447 M 63% | #################### | 24 / 38 [27.17s<43.02s, 0.88 count/s] init 22 axmodel ok,remain_cmm(3361 M 65% | ##################### | 25 / 38 [28.33s<43.06s, 0.88 count/s] init 23 axmodel ok,remain_cmm(3276 M 68% | ##################### | 26 / 38 [29.70s<43.41s, 0.88 count/s] init 24 axmodel ok,remain_cmm(3177 M 71% | ###################### | 27 / 38 [30.89s<43.48s, 0.87 count/s] init 25 axmodel ok,remain_cmm(3092 M 73% | ####################### | 28 / 38 [32.16s<43.65s, 0.87 count/s] init 26 axmodel ok,remain_cmm(3006 M 76% | ######################## | 29 / 38 [33.32s<43.67s, 0.87 count/s] init 27 axmodel ok,remain_cmm(2921 M 78% | ######################### | 30 / 38 [34.43s<43.61s, 0.87 count/s] init 28 axmodel ok,remain_cmm(2836 M 81% | ########################## | 31 / 38 [35.69s<43.75s, 0.87 count/s] init 29 axmodel ok,remain_cmm(2737 M 84% | ########################## | 32 / 38 [36.84s<43.75s, 0.87 count/s] init 30 axmodel ok,remain_cmm(2652 M 86% | ########################### | 33 / 38 [37.75s<43.47s, 0.87 count/s] init 31 axmodel ok,remain_cmm(2566 M 89% | ############################ | 34 / 38 [38.44s<42.96s, 0.88 count/s] init 32 axmodel ok,remain_cmm(2481 M 92% | ############################# | 35 / 38 [39.06s<42.41s, 0.90 count/s] init 33 axmodel ok,remain_cmm(2396 M 94% | ############################## | 36 / 38 [39.44s<41.63s, 0.91 count/s] init 34 axmodel ok,remain_cmm(2297 M 97% | ############################### | 37 / 38 [41.12s<42.23s, 0.90 count/s] init post axmodel ok,remain_cmm(1877 MB)
249
- 16:23:02.455 INF Init:1045 | max_token_len : 2047
250
- 16:23:02.455 INF Init:1048 | kv_cache_size : 256, kv_cache_num: 2047
251
- 16:23:02.455 INF init_groups_from_model:606 | prefill_token_num : 128
252
- 16:23:02.455 INF init_groups_from_model:820 | decode grp: 0, gid: 0, max_token_len : 2047
253
- 16:23:02.455 INF init_groups_from_model:824 | prefill grp: 0, gid: 1, history_cap: 0, total_cap: 128, symbolic_cap: 1
254
- 16:23:02.455 INF init_groups_from_model:824 | prefill grp: 1, gid: 2, history_cap: 128, total_cap: 256, symbolic_cap: 128
255
- 16:23:02.455 INF init_groups_from_model:824 | prefill grp: 2, gid: 3, history_cap: 256, total_cap: 384, symbolic_cap: 256
256
- 16:23:02.455 INF init_groups_from_model:824 | prefill grp: 3, gid: 4, history_cap: 384, total_cap: 512, symbolic_cap: 384
257
- 16:23:02.455 INF init_groups_from_model:824 | prefill grp: 4, gid: 5, history_cap: 512, total_cap: 640, symbolic_cap: 512
258
- 16:23:02.455 INF init_groups_from_model:824 | prefill grp: 5, gid: 6, history_cap: 640, total_cap: 768, symbolic_cap: 640
259
- 16:23:02.455 INF init_groups_from_model:824 | prefill grp: 6, gid: 7, history_cap: 768, total_cap: 896, symbolic_cap: 768
260
- 16:23:02.455 INF init_groups_from_model:824 | prefill grp: 7, gid: 8, history_cap: 896, total_cap: 1024, symbolic_cap: 896
261
- 16:23:02.455 INF init_groups_from_model:824 | prefill grp: 8, gid: 9, history_cap: 1024, total_cap: 1152, symbolic_cap: 1024
262
- 16:23:02.455 INF init_groups_from_model:831 | prefill_max_token_num : 1152
263
- 16:23:02.455 INF Init:27 | LLaMaEmbedSelector use mmap
264
- 100% | ################################ | 38 / 38 [41.12s<41.12s, 0.92 count/s] embed_selector init ok
265
- 16:23:02.472 INF Init:475 | Gemma4 per-layer helper enabled: vocab=262144 hidden=1536 layers=35 per_layer=256 pad=0
266
- 16:23:03.400 INF Init:785 | Gemma4-VL token ids: image_pad=258880 video_pad=258884
267
- 16:23:03.400 INF Init:792 | VisionModule init ok: type=Gemma4VL, tokens_per_block=70, embed_size=1536, out_dtype=fp32
268
- 16:23:03.400 WRN Init:801 | Vision preprocess backend: SimpleCV (OpenCV not found at build time; minor differences vs OpenCV are possible)
269
- 16:23:03.404 INF load_config:282 | load config:
270
- 16:23:03.404 INF load_config:282 | {
271
- 16:23:03.404 INF load_config:282 | "enable_repetition_penalty": false,
272
- 16:23:03.404 INF load_config:282 | "enable_temperature": true,
273
- 16:23:03.404 INF load_config:282 | "enable_top_k_sampling": false,
274
- 16:23:03.404 INF load_config:282 | "enable_top_p_sampling": true,
275
- 16:23:03.404 INF load_config:282 | "penalty_window": 64,
276
- 16:23:03.404 INF load_config:282 | "repetition_penalty": 1.0,
277
- 16:23:03.404 INF load_config:282 | "temperature": 1.0,
278
- 16:23:03.404 INF load_config:282 | "top_k": 64,
279
- 16:23:03.404 INF load_config:282 | "top_p": 0.95
280
- 16:23:03.404 INF load_config:282 | }
281
- 16:23:03.404 INF Init:1139 | LLM init ok
 
 
282
  Starting server on port 8000 with model 'AXERA-TECH/gemma-4-E2B-it'...
283
  API URLs:
284
  GET http://127.0.0.1:8000/health
@@ -329,6 +371,8 @@ root@ax650 ~ # curl http://127.0.0.1:8000/v1/models
329
  }
330
  ```
331
 
 
 
332
  ## Python Runtime Requirements
333
 
334
  Install the following packages on the AX board:
@@ -482,6 +526,59 @@ Here is a detailed description:
482
  In summary, it is a cheerful, stylized, red cartoon lobster flexing its claws.
483
  ```
484
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
  ### Gradio Demo
486
 
487
  ```bash
@@ -501,8 +598,9 @@ The Python demo scripts use the following default paths:
501
  - Tokenizer and config: `./gemma_4_e2b_it_tokenizer`
502
  - Text LLM runtime root: `./`
503
  - Vision axmodels: `./`
 
504
 
505
- If you move any of these directories, pass the new values with `--hf_model`, `--axmodel_path`, and `--vit_model_path`.
506
 
507
  For the Python demo flow, `--axmodel_path` should point to the directory that contains the text runtime files such as `gemma4_text_p128_l*.axmodel`, `gemma4_text_post.axmodel`, `model.embed_tokens.weight.bfloat16.bin`, and the `model.*per_layer*.npy` files.
508
 
 
6
  tags:
7
  - gemma4
8
  - multimodal
9
+ - audio
10
  - axera
11
  - ax650
12
  language:
 
24
  - This release packages the `w8a16` AXERA NPU runtime.
25
  - Compatible with `Pulsar2 5.2` and later.
26
  - Includes the tokenizer/config files required at runtime.
27
+ - Includes compiled Gemma 4 text `.axmodel` files, Vision `.axmodel` files, and fixed-duration Audio `.axmodel` files.
28
+ - Supports text-only chat, single-image multimodal inference, and fixed-duration audio transcription in the legacy Python demo flow.
29
 
30
  ## Supported Platform
31
 
 
43
 
44
  All measurements below were taken on AX650 / NPU3. `TTFT` stands for time to first token.
45
 
46
+ - `w8a16`: TTFT is approximately `2175 ms` (1152 tokens), with a decode throughput of approximately `7.99 tokens/s` (theoretical maximum).
47
+ - `w4a16`: TTFT is approximately `1568 ms` (1152 tokens), with a decode throughput of approximately `12.41 tokens/s` (theoretical maximum).
48
 
49
  The packaged text runtime in this release is the `w8a16` build. Its text runtime files are packaged at the repository root. The `w4a16` numbers are provided for reference only.
50
 
 
56
  | `gemma4_vision_h480_w672_t140.axmodel` | 480×672 | 140 | 258.329 ms |
57
  | `gemma4_vision_h672_w960_t280.axmodel` | 672×960 | 280 | 750.429 ms |
58
 
59
+ ## Audio Encoder Latency and Accuracy
60
+
61
+ Validated on AX650 / NPU3 with the packaged WAV sample clips and the legacy Python demo flow.
62
+
63
+ | Model | Audio Duration | Audio Tokens |
64
+ |---|---|---|
65
+ | `gemma4_audio_5s.axmodel` | `5s` | `125` |
66
+ | `gemma4_audio_30s.axmodel` | `30s` | `750` |
67
+
68
+ Single-run latency measured with `/opt/bin/ax_run_model -w 1 -r 5`:
69
+
70
+ | Model | CMM Size | Avg |
71
+ |---|---|---|
72
+ | `gemma4_audio_5s.axmodel` | `~310 MiB` | `28.930 ms` |
73
+ | `gemma4_audio_30s.axmodel` | `~359 MiB` | `170.978 ms` |
74
+
75
  ## Package Layout
76
 
77
  ```text
 
86
  ├── gemma4_tokenizer.txt
87
  ├── gemma4_text_p128_l*.axmodel
88
  ├── gemma4_text_post.axmodel
89
+ ├── gemma4_audio_5s.axmodel
90
+ ├── gemma4_audio_30s.axmodel
91
  ├── gemma4_vision_h336_w480_t70.axmodel
92
  ├── gemma4_vision_h480_w672_t140.axmodel
93
  ├── gemma4_vision_h672_w960_t280.axmodel
 
99
  └── utils/
100
  ```
101
 
102
+ This package uses a hybrid layout: the tokenizer stays in a subdirectory, the packaged text runtime files plus the Vision and Audio `.axmodel` files live at the repository root, and `vit_models/` keeps the accompanying Vision metadata JSON files.
103
 
104
  The Python demo scripts auto-detect the packaged paths above. If you keep this layout unchanged, you can run the Python examples later in this README without passing extra path arguments.
105
 
 
110
 
111
  ![sample](assets/sample.png)
112
 
113
+ ## Sample Audio
114
+
115
+ The package also includes three packaged WAV clips for board-side audio validation:
116
+
117
+ - `assets/gemma4_audio_test_5s.wav`
118
+ - `assets/gemma4_audio_test_chunk0_30s.wav`
119
+ - `assets/gemma4_audio_test_chunk1_30s.wav`
120
+
121
  ## Direct Inference with `axllm`
122
 
123
  > The `axllm` workflow is still being refined. The instructions below reflect the current validated flow and may be adjusted as the packaging continues to evolve.
 
171
  $ axllm run AXERA-TECH/gemma-4-E2B-it
172
 
173
  # output log example:
174
+ 14:57:27.565 INF Init:1019 | LLM init start
175
+ 14:57:27.565 INF Init:1034 | shared kv enabled: num_kv_shared_layers=20
176
+ 14:57:27.565 INF Init:1050 | attention config: layers=35 sliding=28 full=7 linear=0 sliding_window=512 ref_full_layer_idx=0
177
  tokenizer_type = 3
178
  huggingface tokenizer mode = space_replace_bpe
179
+ 31% | ########## | 12 / 38 [3.41s<10.80s, 3.52 count/s] init 10 axmodel ok,remain_cmm(7282 MB 34% | ########## | 13 / 38 [3.48s<10.18s, 3.73 count/s] init 11 axmodel ok,remain_cmm(7227 MB 97% | ############################### | 37 / 38 [6.57s<6.74s, 5.64 count/s] init post axmodel ok,remain_cmm(4868 MB)
180
+ 14:57:34.130 INF Init:1196 | max_token_len : 2047
181
+ 14:57:34.131 INF Init:1199 | kv_cache_size : 256, kv_cache_num: 2047
182
+ 14:57:34.131 INF init_groups_from_model:702 | prefill_token_num : 128
183
+ 14:57:34.131 INF init_groups_from_model:916 | decode grp: 0, gid: 0, max_token_len : 2047
184
+ 14:57:34.131 INF init_groups_from_model:920 | prefill grp: 0, gid: 1, history_cap: 0, total_cap: 128, symbolic_cap: 1
185
+ 14:57:34.131 INF init_groups_from_model:920 | prefill grp: 1, gid: 2, history_cap: 128, total_cap: 256, symbolic_cap: 128
186
+ 14:57:34.131 INF init_groups_from_model:920 | prefill grp: 2, gid: 3, history_cap: 256, total_cap: 384, symbolic_cap: 256
187
+ 14:57:34.131 INF init_groups_from_model:920 | prefill grp: 3, gid: 4, history_cap: 384, total_cap: 512, symbolic_cap: 384
188
+ 14:57:34.131 INF init_groups_from_model:920 | prefill grp: 4, gid: 5, history_cap: 512, total_cap: 640, symbolic_cap: 512
189
+ 14:57:34.131 INF init_groups_from_model:920 | prefill grp: 5, gid: 6, history_cap: 640, total_cap: 768, symbolic_cap: 640
190
+ 14:57:34.131 INF init_groups_from_model:920 | prefill grp: 6, gid: 7, history_cap: 768, total_cap: 896, symbolic_cap: 768
191
+ 14:57:34.131 INF init_groups_from_model:920 | prefill grp: 7, gid: 8, history_cap: 896, total_cap: 1024, symbolic_cap: 896
192
+ 14:57:34.131 INF init_groups_from_model:920 | prefill grp: 8, gid: 9, history_cap: 1024, total_cap: 1152, symbolic_cap: 1024
193
+ 14:57:34.131 INF init_groups_from_model:927 | prefill_max_token_num : 1152
194
+ 14:57:34.131 INF Init:27 | LLaMaEmbedSelector use mmap
195
+ 100% | ################################ | 38 / 38 [6.57s<6.57s, 5.78 count/s] embed_selector init ok
196
+ 14:57:34.149 INF Init:475 | Gemma4 per-layer helper enabled: vocab=262144 hidden=1536 layers=35 per_layer=256 pad=0
197
+ 14:57:39.791 INF init_audio_profile:245 | Gemma4 audio profile init ok: path=../../gemma-4-E2B-it/gemma4_audio_5s.axmodel duration=5.0s mel_frames=499 tokens=125 out_dtype=fp32
198
+ 14:57:40.049 INF init_audio_profile:245 | Gemma4 audio profile init ok: path=../../gemma-4-E2B-it/gemma4_audio_30s.axmodel duration=30.0s mel_frames=2999 tokens=750 out_dtype=fp32
199
+ 14:57:40.049 INF Init:914 | Gemma4-VL token ids: image_pad=258880 video_pad=258884 audio_pad=258881
200
+ 14:57:40.049 INF Init:921 | VisionModule init ok: type=Gemma4VL, tokens_per_block=70, embed_size=1536, out_dtype=fp32
201
+ 14:57:40.049 WRN Init:930 | Vision preprocess backend: SimpleCV (OpenCV not found at build time; minor differences vs OpenCV are possible)
202
+ 14:57:40.054 INF load_config:444 | load config:
203
+ 14:57:40.054 INF load_config:444 | {
204
+ 14:57:40.054 INF load_config:444 | "enable_repetition_penalty": false,
205
+ 14:57:40.054 INF load_config:444 | "enable_temperature": false,
206
+ 14:57:40.054 INF load_config:444 | "enable_top_k_sampling": false,
207
+ 14:57:40.054 INF load_config:444 | "enable_top_p_sampling": false,
208
+ 14:57:40.054 INF load_config:444 | "penalty_window": 64,
209
+ 14:57:40.054 INF load_config:444 | "repetition_penalty": 1.0,
210
+ 14:57:40.054 INF load_config:444 | "temperature": 1.0,
211
+ 14:57:40.054 INF load_config:444 | "top_k": 64,
212
+ 14:57:40.054 INF load_config:444 | "top_p": 0.95
213
+ 14:57:40.054 INF load_config:444 | }
214
+ 14:57:40.055 INF Init:1293 | LLM init ok
215
  Commands:
216
  /q, /exit 退出
217
  /reset 重置 kvcache
218
  /dd 删除一轮对话
219
  /pp 打印历史对话
220
  Ctrl+C: 停止当前生成
221
+ VLM enabled: after each prompt, input media path (empty = text-only). Use "video:<frames_dir>" for video, "audio:<file>" for audio.
222
  ----------------------------------------
223
+ prompt >> hello, who are you?
224
+ media >>
225
+ 14:58:01.823 INF SetKVCache:1607 | decode_grpid:0 prefill_grpid:1 history_cap:0 total_cap:128 symbolic_cap:1 precompute_len:0 input_num_token:26 prefer_symbolic_group:0
226
+ 14:58:01.823 INF SetKVCache:1628 | current prefill_max_token_num:1152
227
+ 14:58:01.863 INF SetKVCache:1632 | first run
228
+ 14:58:01.884 INF Run:1736 | input token num : 26, prefill_split_num : 1
229
+ 14:58:02.014 INF Run:1819 | prefill chunk p=0 history_len=0 grpid=1 kv_cache_num=0 input_tokens=26
230
+ 14:58:02.014 INF Run:1843 | prefill indices shape: p=0 idx_elems=128 idx_rows=1 pos_rows=0
231
+ 14:58:02.267 INF Run:2028 | ttft: 383.05 ms
232
+ Hello! I am Gemma 4, a Large Language Model developed by Google DeepMind. I am an open weights model. How can I help you today?
233
+
234
+ 14:58:07.195 NTC Run:2396 | hit eos,decode avg 6.29 token/s
235
+ 14:58:07.198 INF GetKVCache:1572 | precompute_len:58, remaining:1094 (tracked)
236
+ prompt >> Okay, great!
237
  media >>
238
+ 14:58:20.884 INF SetKVCache:1607 | decode_grpid:0 prefill_grpid:2 history_cap:128 total_cap:256 symbolic_cap:128 precompute_len:58 input_num_token:14 prefer_symbolic_group:0
239
+ 14:58:20.885 INF SetKVCache:1628 | current prefill_max_token_num:1024
240
+ 14:58:20.892 INF Run:1736 | input token num : 14, prefill_split_num : 1
241
+ 14:58:20.937 INF Run:1819 | prefill chunk p=0 history_len=58 grpid=2 kv_cache_num=128 input_tokens=14
242
+ 14:58:20.938 INF Run:1843 | prefill indices shape: p=0 idx_elems=128 idx_rows=1 pos_rows=0
243
+ 14:58:21.210 INF Run:2028 | ttft: 318.07 ms
244
+ I'm happy to help! What can I do for you? Do you have a question, need some information, want to brainstorm some ideas, or anything else? 😊
245
+
246
+ 14:58:26.377 NTC Run:2396 | hit eos,decode avg 6.58 token/s
247
+ 14:58:26.383 INF GetKVCache:1572 | precompute_len:107, remaining:1045 (tracked)
248
+ prompt >> Please describe this image in detail.
249
+ media >> /root/your/workspace/gemma-4-E2B-it/assets/sample.png
250
+ 15:01:41.584 INF EncodeForContent:1464 | vision cache store: /root/your/workspace/gemma-4-E2B-it/assets/sample.png
251
+ 15:01:41.610 INF SetKVCache:1607 | decode_grpid:0 prefill_grpid:3 history_cap:256 total_cap:384 symbolic_cap:256 precompute_len:107 input_num_token:91 prefer_symbolic_group:1
252
+ 15:01:41.610 INF SetKVCache:1628 | current prefill_max_token_num:1024
253
+ 15:01:41.630 INF Run:1736 | input token num : 91, prefill_split_num : 1
254
+ 15:01:41.922 INF Run:1819 | prefill chunk p=0 history_len=107 grpid=3 kv_cache_num=256 input_tokens=91
255
+ 15:01:41.923 INF Run:1843 | prefill indices shape: p=0 idx_elems=128 idx_rows=1 pos_rows=0
256
+ 15:01:42.232 INF Run:2028 | ttft: 601.69 ms
257
+ This image is a cartoon illustration of a **red, stylized lobster**.
258
+
259
+ Here is a detailed description:
260
+
261
+ * **Subject:** The central subject is a lobster, depicted in a vibrant, bright red color.
262
+ * **Style:** The illustration is highly stylized and cartoonish, featuring thick outlines and exaggerated features, giving it a playful or energetic look.
263
+ * **Pose/Expression:** The lobster appears to be in an active or aggressive pose. It has prominent claws (chelipeds) that are raised, suggesting it might be waving, striking, or ready for action. Its body is curved dynamically.
264
+ * **Details:** You can clearly see the segmented body, the claws, and the legs. The overall design is bold, with clean lines and a glossy or slightly textured appearance typical of a sticker or graphic design.
265
+ * **Background:** The lobster is isolated on a plain white background, which makes the red color and the details of the illustration stand out prominently.
266
+ * **Overall Impression:** The image is energetic, bold, and clearly designed to be eye-catching, likely intended for use as a sticker, icon, or graphic element.
267
+
268
+ 15:02:16.834 NTC Run:2396 | hit eos,decode avg 6.85 token/s
269
+ 15:02:17.013 INF GetKVCache:1572 | precompute_len:436, remaining:716 (tracked)
270
+ prompt >>
 
 
 
 
271
  ```
272
 
273
 
 
280
  ```bash
281
  $ axllm serve AXERA-TECH/gemma-4-E2B-it --port 8000
282
  # output log example:
283
+ 15:05:32.638 INF Init:1019 | LLM init start
284
+ 15:05:32.638 INF Init:1034 | shared kv enabled: num_kv_shared_layers=20
285
+ 15:05:32.638 INF Init:1050 | attention config: layers=35 sliding=28 full=7 linear=0 sliding_window=512 ref_full_layer_idx=0
286
  tokenizer_type = 3
287
  huggingface tokenizer mode = space_replace_bpe
288
+ 31% | ########## | 12 / 38 [3.32s<10.50s, 3.62 count/s] init 10 axmodel ok,remain_cmm(7282 MB 97% | ############################### | 37 / 38 [6.50s<6.67s, 5.69 count/s] init post axmodel ok,remain_cmm(4868 MB)
289
+ 15:05:39.135 INF Init:1196 | max_token_len : 2047
290
+ 15:05:39.135 INF Init:1199 | kv_cache_size : 256, kv_cache_num: 2047
291
+ 15:05:39.135 INF init_groups_from_model:702 | prefill_token_num : 128
292
+ 15:05:39.135 INF init_groups_from_model:916 | decode grp: 0, gid: 0, max_token_len : 2047
293
+ 15:05:39.135 INF init_groups_from_model:920 | prefill grp: 0, gid: 1, history_cap: 0, total_cap: 128, symbolic_cap: 1
294
+ 15:05:39.135 INF init_groups_from_model:920 | prefill grp: 1, gid: 2, history_cap: 128, total_cap: 256, symbolic_cap: 128
295
+ 15:05:39.135 INF init_groups_from_model:920 | prefill grp: 2, gid: 3, history_cap: 256, total_cap: 384, symbolic_cap: 256
296
+ 15:05:39.135 INF init_groups_from_model:920 | prefill grp: 3, gid: 4, history_cap: 384, total_cap: 512, symbolic_cap: 384
297
+ 15:05:39.135 INF init_groups_from_model:920 | prefill grp: 4, gid: 5, history_cap: 512, total_cap: 640, symbolic_cap: 512
298
+ 15:05:39.135 INF init_groups_from_model:920 | prefill grp: 5, gid: 6, history_cap: 640, total_cap: 768, symbolic_cap: 640
299
+ 15:05:39.135 INF init_groups_from_model:920 | prefill grp: 6, gid: 7, history_cap: 768, total_cap: 896, symbolic_cap: 768
300
+ 15:05:39.135 INF init_groups_from_model:920 | prefill grp: 7, gid: 8, history_cap: 896, total_cap: 1024, symbolic_cap: 896
301
+ 15:05:39.135 INF init_groups_from_model:920 | prefill grp: 8, gid: 9, history_cap: 1024, total_cap: 1152, symbolic_cap: 1024
302
+ 15:05:39.135 INF init_groups_from_model:927 | prefill_max_token_num : 1152
303
+ 15:05:39.135 INF Init:27 | LLaMaEmbedSelector use mmap
304
+ 100% | ################################ | 38 / 38 [6.50s<6.50s, 5.85 count/s] embed_selector init ok
305
+ 15:05:39.151 INF Init:475 | Gemma4 per-layer helper enabled: vocab=262144 hidden=1536 layers=35 per_layer=256 pad=0
306
+ 15:05:39.595 INF init_audio_profile:245 | Gemma4 audio profile init ok: path=../../gemma-4-E2B-it/gemma4_audio_5s.axmodel duration=5.0s mel_frames=499 tokens=125 out_dtype=fp32
307
+ 15:05:39.850 INF init_audio_profile:245 | Gemma4 audio profile init ok: path=../../gemma-4-E2B-it/gemma4_audio_30s.axmodel duration=30.0s mel_frames=2999 tokens=750 out_dtype=fp32
308
+ 15:05:39.850 INF Init:914 | Gemma4-VL token ids: image_pad=258880 video_pad=258884 audio_pad=258881
309
+ 15:05:39.850 INF Init:921 | VisionModule init ok: type=Gemma4VL, tokens_per_block=70, embed_size=1536, out_dtype=fp32
310
+ 15:05:39.850 WRN Init:930 | Vision preprocess backend: SimpleCV (OpenCV not found at build time; minor differences vs OpenCV are possible)
311
+ 15:05:39.852 INF load_config:444 | load config:
312
+ 15:05:39.852 INF load_config:444 | {
313
+ 15:05:39.852 INF load_config:444 | "enable_repetition_penalty": false,
314
+ 15:05:39.852 INF load_config:444 | "enable_temperature": false,
315
+ 15:05:39.852 INF load_config:444 | "enable_top_k_sampling": false,
316
+ 15:05:39.852 INF load_config:444 | "enable_top_p_sampling": false,
317
+ 15:05:39.852 INF load_config:444 | "penalty_window": 64,
318
+ 15:05:39.852 INF load_config:444 | "repetition_penalty": 1.0,
319
+ 15:05:39.852 INF load_config:444 | "temperature": 1.0,
320
+ 15:05:39.852 INF load_config:444 | "top_k": 64,
321
+ 15:05:39.852 INF load_config:444 | "top_p": 0.95
322
+ 15:05:39.852 INF load_config:444 | }
323
+ 15:05:39.852 INF Init:1293 | LLM init ok
324
  Starting server on port 8000 with model 'AXERA-TECH/gemma-4-E2B-it'...
325
  API URLs:
326
  GET http://127.0.0.1:8000/health
 
371
  }
372
  ```
373
 
374
+ ![openai_api_demo](assets/openai_api_demo.png)
375
+
376
  ## Python Runtime Requirements
377
 
378
  Install the following packages on the AX board:
 
526
  In summary, it is a cheerful, stylized, red cartoon lobster flexing its claws.
527
  ```
528
 
529
+ ### Audio Inference
530
+
531
+ The package includes two fixed-duration audio encoders:
532
+
533
+ - `gemma4_audio_5s.axmodel` for `5s / 125` audio tokens
534
+ - `gemma4_audio_30s.axmodel` for `30s / 750` audio tokens
535
+
536
+ For board-side validation, use the packaged WAV clips listed in the sample-audio section above.
537
+
538
+ Example: `5s` profile
539
+
540
+ ```bash
541
+ python3 infer_axmodel.py \
542
+ --audio_path ./assets/gemma4_audio_test_5s.wav \
543
+ --audio_model_path ./gemma4_audio_5s.axmodel \
544
+ --audio_duration_sec 5 \
545
+ --audio_tokens 125 \
546
+ --system_prompt "" \
547
+ --prompt "Transcribe the speech in its original language. Output only the transcription." \
548
+ --max_new_tokens 128
549
+ ```
550
+
551
+ Typical output:
552
+
553
+ ```sh
554
+ answer >> When I was seventeen,I read a quote that went something like if you ...
555
+ ```
556
+
557
+ Example: `30s` profile
558
+
559
+ ```bash
560
+ python3 infer_axmodel.py \
561
+ --audio_path ./assets/gemma4_audio_test_chunk0_30s.wav \
562
+ --audio_model_path ./gemma4_audio_30s.axmodel \
563
+ --audio_duration_sec 30 \
564
+ --audio_tokens 750 \
565
+ --system_prompt "" \
566
+ --prompt "Transcribe the speech in its original language. Output only the transcription." \
567
+ --max_new_tokens 256
568
+ ```
569
+
570
+ Typical output:
571
+
572
+ ```sh
573
+ answer >> No one wants to die. Even people who want to go to heaven don't want to die. Even people who want to go to heaven don't want to die. No one wants to die. Death is the destination we all share and yet death is the destination we all share and no one has ever escaped it and that is as it should be because death is the single best invention of life death is life change out the old make way for the new right now the new is you but someday not too long from now you^@ will gradually become ...
574
+ ```
575
+
576
+ Notes:
577
+
578
+ - The two commands above were validated on board with a `gemma4_pydeps` dependency overlay added to `PYTHONPATH`.
579
+ - The `30s / 750-token` profile spans multiple `128-token` prefill slices. The runtime will print a warning about partial bidirectional attention across earlier slices inside the same multimodal block. This is expected for chunked prefill.
580
+ - The Python demo loader handles `WAV` directly with the Python standard library. For `mp3 / flac / m4a / ogg`, install `librosa` on the board.
581
+
582
  ### Gradio Demo
583
 
584
  ```bash
 
598
  - Tokenizer and config: `./gemma_4_e2b_it_tokenizer`
599
  - Text LLM runtime root: `./`
600
  - Vision axmodels: `./`
601
+ - Audio axmodels: `./`
602
 
603
+ If you move any of these directories, pass the new values with `--hf_model`, `--axmodel_path`, `--vit_model_path`, and `--audio_model_path`.
604
 
605
  For the Python demo flow, `--axmodel_path` should point to the directory that contains the text runtime files such as `gemma4_text_p128_l*.axmodel`, `gemma4_text_post.axmodel`, `model.embed_tokens.weight.bfloat16.bin`, and the `model.*per_layer*.npy` files.
606
 
assets/gemma4_audio_test_5s.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e7d024be5d1358d1e47f8009a2dc642418ab182f572e3804bfd9b8169d11026a
3
+ size 882294
assets/gemma4_audio_test_chunk0_30s.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ddc9d854e322bec2eac0cd5be7603a159fb0a755fe42846b1c6e550aa897bae8
3
+ size 5292294
assets/gemma4_audio_test_chunk1_30s.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9eff5c86060bd784cb9ce6b6597e640b56dd8b256b09b795a70c4df4a711c6d4
3
+ size 5292294
assets/openai_api_demo.png ADDED

Git LFS Details

  • SHA256: 28af9b42463c5f4b31b6c3a52a4d17a0f62399013f4f5a9c54b1fd812de09449
  • Pointer size: 131 Bytes
  • Size of remote file: 615 kB
gemma4_audio_30s.axmodel ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1611607462f9da1b6eb0418e652b1bdbba77dc2756f0e38b5d58609f9c91c416
3
+ size 326792426
gemma4_audio_5s.axmodel ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5263bc9b727590e7cc8c825837596c56b851d9c63c376c24dfbe117bef9de4d5
3
+ size 321409478
infer_axmodel.py CHANGED
@@ -8,11 +8,13 @@ from ml_dtypes import bfloat16
8
  from utils.gemma4_compat import load_text_runtime_config
9
  from utils.gemma4_compat import load_tokenizer
10
  from utils.gemma4_multimodal import DEFAULT_MAX_SOFT_TOKENS
 
11
  from utils.gemma4_multimodal import build_messages
12
  from utils.gemma4_multimodal import detect_soft_tokens_from_vit_path
13
  from utils.gemma4_multimodal import load_image
14
  from utils.gemma4_multimodal import load_processor
15
  from utils.gemma4_multimodal import prepare_multimodal_inputs
 
16
  from utils.gemma4_multimodal import replace_image_tokens
17
  from utils.gemma4_multimodal import resolve_resize
18
  from utils.gemma4_multimodal import to_numpy_fp32
@@ -20,6 +22,8 @@ from utils.gemma4_per_layer import Gemma4PerLayerInputs
20
  from utils.runtime_layout import default_axmodel_path
21
  from utils.runtime_layout import load_text_embeddings
22
  from utils.infer_func import InferManager
 
 
23
  from utils.vision_output import describe_output_shapes
24
  from utils.vision_output import select_vit_output
25
 
@@ -60,14 +64,46 @@ def _default_vit_model_path() -> str:
60
  return str(candidates[0])
61
 
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
  def _run_vit_axmodel(vit_model_path: str, pixel_values: np.ndarray, target_hidden_size: int, expected_tokens: int):
65
  from axengine import InferenceSession
66
 
67
  session = InferenceSession(vit_model_path)
68
- outputs = session.run(None, {"pixel_values": pixel_values})
69
- if isinstance(outputs, dict):
70
- outputs = list(outputs.values())
 
 
 
 
 
71
  return (
72
  select_vit_output(outputs, target_hidden_size, expected_tokens=expected_tokens),
73
  describe_output_shapes(outputs),
@@ -88,6 +124,48 @@ def _run_vit_onnx(vit_model_path: str, pixel_values: np.ndarray, target_hidden_s
88
  )
89
 
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  def _text_prefill_inputs(tokenizer, embeds: np.ndarray, prompt: str, system_prompt: str = "", enable_thinking: bool = False):
92
  messages = build_messages(prompt=prompt, system_prompt=system_prompt)
93
  text = tokenizer.apply_chat_template(
@@ -110,8 +188,13 @@ if __name__ == "__main__":
110
  help="Path to the packaged LLM runtime root or legacy axmodel folder")
111
  parser.add_argument("--vit_model_path", type=str, default=_default_vit_model_path(),
112
  help="Path to Gemma 4 vision ONNX model or .axmodel")
 
 
 
113
  parser.add_argument("--image_path", type=str, default="",
114
  help="Optional input image path. If omitted, runs text-only generation.")
 
 
115
  parser.add_argument("--system_prompt", type=str, default="You are a helpful assistant.",
116
  help="Optional system prompt")
117
  parser.add_argument("--prompt", type=str, default="Describe the image in detail.",
@@ -126,10 +209,41 @@ if __name__ == "__main__":
126
  help="Optional fixed image height for the vision encoder")
127
  parser.add_argument("--resize_w", type=int, default=None,
128
  help="Optional fixed image width for the vision encoder")
129
- parser.add_argument("--slice_len", type=int, default=128,
130
- help="Prefill slice length. Must match the LLM `--prefill_len` used at build time.")
 
 
 
 
 
 
 
 
 
 
 
 
131
  args = parser.parse_args()
132
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  config = load_text_runtime_config(args.hf_model)
134
  embeds = load_text_embeddings(args.axmodel_path, config)
135
  per_layer_helper = None
@@ -143,9 +257,6 @@ if __name__ == "__main__":
143
  print(f"[INFO] Auto-detected max_soft_tokens={detected} from VIT model: {args.vit_model_path}")
144
  args.max_soft_tokens = detected
145
 
146
- kv_cache_len = int(getattr(config, "kv_cache_len", 2047) or 2047)
147
- imer = InferManager(config, args.axmodel_path, max_seq_len=kv_cache_len, per_layer_helper=per_layer_helper)
148
-
149
  mm_token_type_ids = None
150
  prefill_per_layer_inputs = None
151
  if args.image_path:
@@ -204,6 +315,71 @@ if __name__ == "__main__":
204
  image_embeds,
205
  image_token_id=config.image_token_id,
206
  ).astype(bfloat16)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  else:
208
  tokenizer = load_tokenizer(args.hf_model)
209
  token_ids, prefill_data = _text_prefill_inputs(
@@ -232,20 +408,25 @@ if __name__ == "__main__":
232
 
233
  eos_token_id = config.eos_token_id if isinstance(config.eos_token_id, list) else None
234
 
235
- token_ids = imer.prefill(
236
- tokenizer,
237
- token_ids,
238
- prefill_data,
239
- mm_token_type_ids=mm_token_type_ids,
240
- slice_len=args.slice_len,
241
- per_layer_inputs=prefill_per_layer_inputs,
242
- )
243
- imer.decode(
244
- tokenizer,
245
- token_ids,
246
- embeds,
247
- slice_len=args.slice_len,
248
- eos_token_id=eos_token_id,
249
- max_new_tokens=args.max_new_tokens,
250
- )
 
 
 
 
 
251
  print("\n")
 
8
  from utils.gemma4_compat import load_text_runtime_config
9
  from utils.gemma4_compat import load_tokenizer
10
  from utils.gemma4_multimodal import DEFAULT_MAX_SOFT_TOKENS
11
+ from utils.gemma4_multimodal import prepare_audio_inputs
12
  from utils.gemma4_multimodal import build_messages
13
  from utils.gemma4_multimodal import detect_soft_tokens_from_vit_path
14
  from utils.gemma4_multimodal import load_image
15
  from utils.gemma4_multimodal import load_processor
16
  from utils.gemma4_multimodal import prepare_multimodal_inputs
17
+ from utils.gemma4_multimodal import replace_audio_tokens
18
  from utils.gemma4_multimodal import replace_image_tokens
19
  from utils.gemma4_multimodal import resolve_resize
20
  from utils.gemma4_multimodal import to_numpy_fp32
 
22
  from utils.runtime_layout import default_axmodel_path
23
  from utils.runtime_layout import load_text_embeddings
24
  from utils.infer_func import InferManager
25
+ from utils.infer_func import detect_prefill_len
26
+ from utils.infer_func import release_ax_inference_session
27
  from utils.vision_output import describe_output_shapes
28
  from utils.vision_output import select_vit_output
29
 
 
64
  return str(candidates[0])
65
 
66
 
67
+ def _default_audio_profile() -> tuple[str, float, int]:
68
+ script_dir = Path(__file__).resolve().parent
69
+ candidates = [
70
+ (script_dir / "gemma4_audio_30s.axmodel", 30.0, 750),
71
+ (script_dir / "gemma4_audio_5s.axmodel", 5.0, 125),
72
+ (script_dir / "audio_models" / "gemma4_audio_30s.axmodel", 30.0, 750),
73
+ (script_dir / "audio_models" / "gemma4_audio_5s.axmodel", 5.0, 125),
74
+ (script_dir.parent / "model_convert" / "compiled_output_audio_30s" / "compiled.axmodel", 30.0, 750),
75
+ (script_dir.parent / "model_convert" / "compiled_output_audio_5s" / "compiled.axmodel", 5.0, 125),
76
+ (Path("/tmp/compiled_output_audio_30s/compiled.axmodel"), 30.0, 750),
77
+ (Path("/tmp/compiled_output_audio_5s/compiled.axmodel"), 5.0, 125),
78
+ ]
79
+ for candidate, duration_sec, audio_tokens in candidates:
80
+ if candidate.exists():
81
+ return str(candidate), duration_sec, audio_tokens
82
+ return str(candidates[0][0]), candidates[0][1], candidates[0][2]
83
+
84
+
85
+ def _infer_audio_profile_from_path(audio_model_path: str) -> tuple[float | None, int | None]:
86
+ path_str = str(audio_model_path)
87
+ if "audio_5s" in path_str or "compiled_output_audio_5s" in path_str:
88
+ return 5.0, 125
89
+ if "audio_30s" in path_str or "compiled_output_audio_30s" in path_str:
90
+ return 30.0, 750
91
+ return None, None
92
+
93
+
94
 
95
  def _run_vit_axmodel(vit_model_path: str, pixel_values: np.ndarray, target_hidden_size: int, expected_tokens: int):
96
  from axengine import InferenceSession
97
 
98
  session = InferenceSession(vit_model_path)
99
+ try:
100
+ outputs = session.run(None, {"pixel_values": pixel_values})
101
+ if isinstance(outputs, dict):
102
+ outputs = [np.array(value, copy=True) for value in outputs.values()]
103
+ else:
104
+ outputs = [np.array(value, copy=True) for value in outputs]
105
+ finally:
106
+ release_ax_inference_session(session)
107
  return (
108
  select_vit_output(outputs, target_hidden_size, expected_tokens=expected_tokens),
109
  describe_output_shapes(outputs),
 
124
  )
125
 
126
 
127
+ def _run_audio_axmodel(
128
+ audio_model_path: str,
129
+ input_features: np.ndarray,
130
+ target_hidden_size: int,
131
+ expected_tokens: int,
132
+ ):
133
+ from axengine import InferenceSession
134
+
135
+ session = InferenceSession(audio_model_path)
136
+ try:
137
+ outputs = session.run(None, {"input_features": input_features})
138
+ if isinstance(outputs, dict):
139
+ outputs = [np.array(value, copy=True) for value in outputs.values()]
140
+ else:
141
+ outputs = [np.array(value, copy=True) for value in outputs]
142
+ finally:
143
+ release_ax_inference_session(session)
144
+ return (
145
+ select_vit_output(outputs, target_hidden_size, expected_tokens=expected_tokens),
146
+ describe_output_shapes(outputs),
147
+ )
148
+
149
+
150
+ def _run_audio_onnx(
151
+ audio_model_path: str,
152
+ input_features: np.ndarray,
153
+ target_hidden_size: int,
154
+ expected_tokens: int,
155
+ ):
156
+ import onnxruntime as ort
157
+
158
+ providers = ["CPUExecutionProvider"]
159
+ if "CUDAExecutionProvider" in ort.get_available_providers():
160
+ providers = ["CUDAExecutionProvider", "CPUExecutionProvider"]
161
+ session = ort.InferenceSession(audio_model_path, providers=providers)
162
+ outputs = session.run(None, {"input_features": input_features})
163
+ return (
164
+ select_vit_output(outputs, target_hidden_size, expected_tokens=expected_tokens),
165
+ describe_output_shapes(outputs),
166
+ )
167
+
168
+
169
  def _text_prefill_inputs(tokenizer, embeds: np.ndarray, prompt: str, system_prompt: str = "", enable_thinking: bool = False):
170
  messages = build_messages(prompt=prompt, system_prompt=system_prompt)
171
  text = tokenizer.apply_chat_template(
 
188
  help="Path to the packaged LLM runtime root or legacy axmodel folder")
189
  parser.add_argument("--vit_model_path", type=str, default=_default_vit_model_path(),
190
  help="Path to Gemma 4 vision ONNX model or .axmodel")
191
+ default_audio_model_path, default_audio_duration_sec, default_audio_tokens = _default_audio_profile()
192
+ parser.add_argument("--audio_model_path", type=str, default=default_audio_model_path,
193
+ help="Path to Gemma 4 audio ONNX model or .axmodel")
194
  parser.add_argument("--image_path", type=str, default="",
195
  help="Optional input image path. If omitted, runs text-only generation.")
196
+ parser.add_argument("--audio_path", type=str, default="",
197
+ help="Optional input audio path. Audio-only inference uses the fixed-duration audio encoder path.")
198
  parser.add_argument("--system_prompt", type=str, default="You are a helpful assistant.",
199
  help="Optional system prompt")
200
  parser.add_argument("--prompt", type=str, default="Describe the image in detail.",
 
209
  help="Optional fixed image height for the vision encoder")
210
  parser.add_argument("--resize_w", type=int, default=None,
211
  help="Optional fixed image width for the vision encoder")
212
+ parser.add_argument("--slice_len", type=int, default=None,
213
+ help="Prefill slice length. Must match the LLM `--prefill_len` used at build time. "
214
+ "When omitted, auto-detected from axmodel filenames under --axmodel_path.")
215
+ parser.add_argument("--audio_duration_sec", type=float, default=None,
216
+ help="Fixed audio duration expected by the audio encoder model. "
217
+ "When omitted, infer it from --audio_model_path.")
218
+ parser.add_argument("--audio_tokens", type=int, default=None,
219
+ help="Fixed number of audio soft tokens produced by the audio encoder model. "
220
+ "When omitted, infer it from --audio_model_path.")
221
+ parser.add_argument("--audio_embeds_npy", type=str, default="",
222
+ help="Optional path to a pre-computed (1, audio_tokens, hidden_size) or "
223
+ "(audio_tokens, hidden_size) float32 npy. When set, the audio encoder step "
224
+ "(axmodel / onnx) is bypassed and these embeds are used directly for prefill. "
225
+ "Useful for A/B testing torch-reference audio embeds against the on-device audio axmodel.")
226
  args = parser.parse_args()
227
 
228
+ if args.slice_len is None:
229
+ args.slice_len = detect_prefill_len(args.axmodel_path, default=128)
230
+ print(f"[INFO] Auto-detected slice_len={args.slice_len} from {args.axmodel_path}")
231
+
232
+ if args.audio_duration_sec is None or args.audio_tokens is None:
233
+ inferred_duration_sec, inferred_audio_tokens = _infer_audio_profile_from_path(args.audio_model_path)
234
+ if args.audio_duration_sec is None:
235
+ args.audio_duration_sec = inferred_duration_sec or default_audio_duration_sec
236
+ if args.audio_tokens is None:
237
+ args.audio_tokens = inferred_audio_tokens or default_audio_tokens
238
+
239
+ if args.image_path and args.audio_path:
240
+ # Gemma4 natively supports image+audio in the same prompt; this script's prefill
241
+ # path currently only wires up a single modality's prepare_* helper. Lifting this
242
+ # requires extending prepare_* to produce a combined inputs dict with both
243
+ # pixel_values and input_features, and running both encoders before replacing
244
+ # tokens. Left as future work.
245
+ raise ValueError("Simultaneous image+audio inputs are not supported by this demo script yet.")
246
+
247
  config = load_text_runtime_config(args.hf_model)
248
  embeds = load_text_embeddings(args.axmodel_path, config)
249
  per_layer_helper = None
 
257
  print(f"[INFO] Auto-detected max_soft_tokens={detected} from VIT model: {args.vit_model_path}")
258
  args.max_soft_tokens = detected
259
 
 
 
 
260
  mm_token_type_ids = None
261
  prefill_per_layer_inputs = None
262
  if args.image_path:
 
315
  image_embeds,
316
  image_token_id=config.image_token_id,
317
  ).astype(bfloat16)
318
+ elif args.audio_path:
319
+ processor = load_processor(args.hf_model)
320
+ tokenizer = processor.tokenizer
321
+ mm = prepare_audio_inputs(
322
+ processor,
323
+ audio_path=args.audio_path,
324
+ prompt=args.prompt,
325
+ system_prompt=args.system_prompt,
326
+ enable_thinking=args.enable_thinking,
327
+ audio_duration_sec=args.audio_duration_sec,
328
+ fixed_audio_tokens=args.audio_tokens,
329
+ )
330
+ inputs = mm["inputs"]
331
+ token_ids = inputs["input_ids"][0].cpu().numpy().tolist()
332
+ mm_token_type_ids = inputs["mm_token_type_ids"][0].cpu().numpy().tolist()
333
+ input_features = to_numpy_fp32(inputs["input_features"])
334
+
335
+ if args.audio_tokens > args.slice_len:
336
+ print(
337
+ f"[WARN] audio_tokens={args.audio_tokens} exceeds slice_len={args.slice_len}. "
338
+ "Cross-slice audio blocks are supported, but the current chunked prefill path does not provide "
339
+ "full future-token visibility to earlier slices within the same multimodal block."
340
+ )
341
+
342
+ if args.audio_embeds_npy:
343
+ audio_embeds = np.load(args.audio_embeds_npy).astype(np.float32)
344
+ audio_output_shapes = [tuple(int(v) for v in audio_embeds.shape)]
345
+ print(f"[INFO] loaded precomputed audio_embeds from {args.audio_embeds_npy}, shape={audio_embeds.shape}")
346
+ elif not os.path.exists(args.audio_model_path):
347
+ raise FileNotFoundError(
348
+ f"Audio model not found: {args.audio_model_path}. "
349
+ "Please export and compile the Gemma 4 audio encoder first, "
350
+ "or pass --audio_embeds_npy to bypass the audio encoder."
351
+ )
352
+ elif args.audio_model_path.endswith(".axmodel"):
353
+ audio_embeds, audio_output_shapes = _run_audio_axmodel(
354
+ args.audio_model_path,
355
+ input_features,
356
+ target_hidden_size=config.hidden_size,
357
+ expected_tokens=args.audio_tokens,
358
+ )
359
+ else:
360
+ audio_embeds, audio_output_shapes = _run_audio_onnx(
361
+ args.audio_model_path,
362
+ input_features,
363
+ target_hidden_size=config.hidden_size,
364
+ expected_tokens=args.audio_tokens,
365
+ )
366
+
367
+ if audio_embeds.ndim == 3:
368
+ audio_embeds = audio_embeds[0]
369
+ if audio_embeds.shape[0] != args.audio_tokens:
370
+ raise ValueError(
371
+ "Unexpected audio output token count. "
372
+ f"got={audio_embeds.shape[0]}, expected={args.audio_tokens}, "
373
+ f"audio_output_shapes={audio_output_shapes}"
374
+ )
375
+
376
+ prefill_data = np.take(embeds, token_ids, axis=0)
377
+ prefill_data = replace_audio_tokens(
378
+ token_ids,
379
+ prefill_data,
380
+ audio_embeds,
381
+ audio_token_id=config.audio_token_id,
382
+ ).astype(bfloat16)
383
  else:
384
  tokenizer = load_tokenizer(args.hf_model)
385
  token_ids, prefill_data = _text_prefill_inputs(
 
408
 
409
  eos_token_id = config.eos_token_id if isinstance(config.eos_token_id, list) else None
410
 
411
+ kv_cache_len = int(getattr(config, "kv_cache_len", 2047) or 2047)
412
+ imer = InferManager(config, args.axmodel_path, max_seq_len=kv_cache_len, per_layer_helper=per_layer_helper)
413
+ try:
414
+ token_ids = imer.prefill(
415
+ tokenizer,
416
+ token_ids,
417
+ prefill_data,
418
+ mm_token_type_ids=mm_token_type_ids,
419
+ slice_len=args.slice_len,
420
+ per_layer_inputs=prefill_per_layer_inputs,
421
+ )
422
+ imer.decode(
423
+ tokenizer,
424
+ token_ids,
425
+ embeds,
426
+ slice_len=args.slice_len,
427
+ eos_token_id=eos_token_id,
428
+ max_new_tokens=args.max_new_tokens,
429
+ )
430
+ finally:
431
+ imer.close()
432
  print("\n")
post_config.json CHANGED
@@ -1,10 +1,10 @@
1
  {
2
- "enable_temperature": true,
3
  "temperature": 1.0,
4
  "enable_repetition_penalty": false,
5
  "repetition_penalty": 1.0,
6
  "penalty_window": 64,
7
- "enable_top_p_sampling": true,
8
  "top_p": 0.95,
9
  "enable_top_k_sampling": false,
10
  "top_k": 64
 
1
  {
2
+ "enable_temperature": false,
3
  "temperature": 1.0,
4
  "enable_repetition_penalty": false,
5
  "repetition_penalty": 1.0,
6
  "penalty_window": 64,
7
+ "enable_top_p_sampling": false,
8
  "top_p": 0.95,
9
  "enable_top_k_sampling": false,
10
  "top_k": 64
utils/gemma4_compat.py CHANGED
@@ -13,6 +13,7 @@ def load_text_runtime_config(model_dir: str):
13
  text_config = dict(raw_config.get("text_config") or raw_config)
14
  text_config["model_type"] = text_config.get("model_type", "gemma4_text")
15
  text_config["eos_token_id"] = raw_config.get("eos_token_id", text_config.get("eos_token_id"))
 
16
  text_config["image_token_id"] = raw_config.get("image_token_id")
17
  text_config["vision_config"] = raw_config.get("vision_config")
18
  text_config["vision_soft_tokens_per_image"] = raw_config.get("vision_soft_tokens_per_image")
 
13
  text_config = dict(raw_config.get("text_config") or raw_config)
14
  text_config["model_type"] = text_config.get("model_type", "gemma4_text")
15
  text_config["eos_token_id"] = raw_config.get("eos_token_id", text_config.get("eos_token_id"))
16
+ text_config["audio_token_id"] = raw_config.get("audio_token_id")
17
  text_config["image_token_id"] = raw_config.get("image_token_id")
18
  text_config["vision_config"] = raw_config.get("vision_config")
19
  text_config["vision_soft_tokens_per_image"] = raw_config.get("vision_soft_tokens_per_image")
utils/gemma4_multimodal.py CHANGED
@@ -1,6 +1,8 @@
1
  from __future__ import annotations
2
 
 
3
  from pathlib import Path
 
4
 
5
  import numpy as np
6
  import torch
@@ -103,25 +105,86 @@ def load_image(image_path: str | Path) -> Image.Image:
103
  return Image.open(image_path).convert("RGB")
104
 
105
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  def resize_image(image: Image.Image, resize_h: int, resize_w: int) -> Image.Image:
107
  return image.convert("RGB").resize((resize_w, resize_h), resample=Image.BICUBIC)
108
 
109
 
110
- def build_messages(prompt: str, image: Image.Image | None = None, system_prompt: str = "") -> list[dict]:
 
 
 
 
 
111
  messages: list[dict] = []
112
  if system_prompt.strip():
113
  messages.append({"role": "system", "content": system_prompt})
114
 
115
- if image is None:
116
  messages.append({"role": "user", "content": prompt})
117
  else:
 
 
 
 
 
 
118
  messages.append(
119
  {
120
  "role": "user",
121
- "content": [
122
- {"type": "image", "image": image},
123
- {"type": "text", "text": prompt},
124
- ],
125
  }
126
  )
127
  return messages
@@ -130,6 +193,7 @@ def build_messages(prompt: str, image: Image.Image | None = None, system_prompt:
130
  def build_processor_messages(
131
  prompt: str,
132
  image: Image.Image | None = None,
 
133
  system_prompt: str = "",
134
  history=None,
135
  ) -> list[dict]:
@@ -157,6 +221,8 @@ def build_processor_messages(
157
  messages.append({"role": "assistant", "content": bot_msg})
158
 
159
  user_content = []
 
 
160
  if image is not None:
161
  user_content.append({"type": "image", "image": image})
162
  user_content.append({"type": "text", "text": prompt})
@@ -261,25 +327,107 @@ def prepare_multimodal_inputs(
261
  }
262
 
263
 
264
- def replace_image_tokens(token_ids, token_embeds, image_embeds, image_token_id: int):
265
- image_positions = [idx for idx, token_id in enumerate(token_ids) if int(token_id) == int(image_token_id)]
266
- if not image_positions:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  return token_embeds
268
 
269
- flat_image_embeds = image_embeds.reshape(-1, image_embeds.shape[-1])
270
- if len(image_positions) != flat_image_embeds.shape[0]:
271
  raise ValueError(
272
- f"Image tokens and image features do not match: tokens={len(image_positions)}, "
273
- f"features={flat_image_embeds.shape[0]}"
274
  )
275
- if token_embeds.shape[-1] != flat_image_embeds.shape[-1]:
276
  raise ValueError(
277
- f"Embedding dim mismatch: token_dim={token_embeds.shape[-1]}, image_dim={flat_image_embeds.shape[-1]}"
 
278
  )
279
- token_embeds[image_positions, :] = flat_image_embeds
280
  return token_embeds
281
 
282
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  def to_numpy_fp32(tensor_like) -> np.ndarray:
284
  if isinstance(tensor_like, np.ndarray):
285
  return tensor_like.astype(np.float32)
 
1
  from __future__ import annotations
2
 
3
+ import os
4
  from pathlib import Path
5
+ import wave
6
 
7
  import numpy as np
8
  import torch
 
105
  return Image.open(image_path).convert("RGB")
106
 
107
 
108
+ def _resample_waveform(waveform: np.ndarray, src_rate: int, dst_rate: int) -> np.ndarray:
109
+ # Linear interpolation; introduces aliasing when downsampling (e.g. 44.1kHz -> 16kHz).
110
+ # For best quality pass mono 16kHz WAV and skip this path; otherwise librosa.load
111
+ # with its polyphase resampler is preferred.
112
+ if src_rate == dst_rate or waveform.size == 0:
113
+ return waveform.astype(np.float32)
114
+
115
+ src_positions = np.arange(waveform.shape[0], dtype=np.float32) / float(src_rate)
116
+ dst_length = max(1, int(round(waveform.shape[0] * float(dst_rate) / float(src_rate))))
117
+ dst_positions = np.arange(dst_length, dtype=np.float32) / float(dst_rate)
118
+ return np.interp(dst_positions, src_positions, waveform).astype(np.float32)
119
+
120
+
121
+ def load_audio_waveform(audio_path: str | Path, sampling_rate: int = 16000) -> np.ndarray:
122
+ audio_path = Path(audio_path)
123
+ if audio_path.suffix.lower() == ".wav":
124
+ with wave.open(str(audio_path), "rb") as wav_file:
125
+ if wav_file.getcomptype() != "NONE":
126
+ raise ValueError(f"Unsupported WAV compression type: {wav_file.getcomptype()}")
127
+
128
+ channels = wav_file.getnchannels()
129
+ sample_width = wav_file.getsampwidth()
130
+ src_rate = wav_file.getframerate()
131
+ frames = wav_file.readframes(wav_file.getnframes())
132
+
133
+ if sample_width == 1:
134
+ waveform = np.frombuffer(frames, dtype=np.uint8).astype(np.float32)
135
+ waveform = (waveform - 128.0) / 128.0
136
+ elif sample_width == 2:
137
+ waveform = np.frombuffer(frames, dtype="<i2").astype(np.float32) / 32768.0
138
+ elif sample_width == 4:
139
+ waveform = np.frombuffer(frames, dtype="<i4").astype(np.float32) / 2147483648.0
140
+ else:
141
+ raise ValueError(f"Unsupported WAV sample width: {sample_width}")
142
+
143
+ if channels > 1:
144
+ waveform = waveform.reshape(-1, channels).mean(axis=1)
145
+ waveform = _resample_waveform(waveform, src_rate=src_rate, dst_rate=sampling_rate)
146
+ return np.asarray(np.clip(waveform, -1.0, 1.0), dtype=np.float32)
147
+
148
+ try:
149
+ os.environ.setdefault("NUMBA_DISABLE_JIT", "1")
150
+ os.environ.setdefault("NUMBA_CACHE_DIR", "/tmp/numba_cache")
151
+ import librosa
152
+ except Exception as exc:
153
+ raise RuntimeError(
154
+ "Non-WAV audio loading requires librosa. Please convert the input to a mono 16kHz WAV file."
155
+ ) from exc
156
+
157
+ waveform, _ = librosa.load(str(audio_path), sr=sampling_rate, mono=True)
158
+ return np.asarray(waveform, dtype=np.float32)
159
+
160
+
161
  def resize_image(image: Image.Image, resize_h: int, resize_w: int) -> Image.Image:
162
  return image.convert("RGB").resize((resize_w, resize_h), resample=Image.BICUBIC)
163
 
164
 
165
+ def build_messages(
166
+ prompt: str,
167
+ image: Image.Image | None = None,
168
+ audio=None,
169
+ system_prompt: str = "",
170
+ ) -> list[dict]:
171
  messages: list[dict] = []
172
  if system_prompt.strip():
173
  messages.append({"role": "system", "content": system_prompt})
174
 
175
+ if image is None and audio is None:
176
  messages.append({"role": "user", "content": prompt})
177
  else:
178
+ user_content = []
179
+ if audio is not None:
180
+ user_content.append({"type": "audio", "audio": audio})
181
+ if image is not None:
182
+ user_content.append({"type": "image", "image": image})
183
+ user_content.append({"type": "text", "text": prompt})
184
  messages.append(
185
  {
186
  "role": "user",
187
+ "content": user_content,
 
 
 
188
  }
189
  )
190
  return messages
 
193
  def build_processor_messages(
194
  prompt: str,
195
  image: Image.Image | None = None,
196
+ audio=None,
197
  system_prompt: str = "",
198
  history=None,
199
  ) -> list[dict]:
 
221
  messages.append({"role": "assistant", "content": bot_msg})
222
 
223
  user_content = []
224
+ if audio is not None:
225
+ user_content.append({"type": "audio", "audio": audio})
226
  if image is not None:
227
  user_content.append({"type": "image", "image": image})
228
  user_content.append({"type": "text", "text": prompt})
 
327
  }
328
 
329
 
330
+ def prepare_audio_inputs(
331
+ processor,
332
+ audio_path: str | Path,
333
+ prompt: str,
334
+ system_prompt: str = "",
335
+ enable_thinking: bool = False,
336
+ audio_duration_sec: float = 30.0,
337
+ fixed_audio_tokens: int | None = None,
338
+ ):
339
+ sampling_rate = int(getattr(processor.feature_extractor, "sampling_rate", 16000))
340
+ max_length = int(round(audio_duration_sec * sampling_rate))
341
+ waveform = load_audio_waveform(audio_path, sampling_rate=sampling_rate)
342
+
343
+ if waveform.shape[0] < max_length:
344
+ padded_waveform = np.pad(waveform, (0, max_length - waveform.shape[0]), mode="constant")
345
+ else:
346
+ padded_waveform = waveform[:max_length]
347
+
348
+ messages = build_processor_messages(prompt=prompt, audio=padded_waveform, system_prompt=system_prompt)
349
+ inputs = _safe_apply_chat_template(
350
+ processor,
351
+ messages,
352
+ tokenize=True,
353
+ return_dict=True,
354
+ return_tensors="pt",
355
+ add_generation_prompt=True,
356
+ enable_thinking=enable_thinking,
357
+ processor_kwargs={
358
+ "audio_kwargs": {
359
+ "padding": "max_length",
360
+ "max_length": max_length,
361
+ "truncation": True,
362
+ "pad_to_multiple_of": None,
363
+ }
364
+ },
365
+ )
366
+
367
+ audio_token_id = processor.audio_token_id
368
+ audio_token_count = int((inputs["input_ids"] == audio_token_id).sum().item())
369
+ expected_tokens = int(fixed_audio_tokens or processor.audio_seq_length)
370
+ if audio_token_count != expected_tokens:
371
+ raise ValueError(
372
+ f"Expected {expected_tokens} audio soft tokens from fixed audio preprocessing, got {audio_token_count}."
373
+ )
374
+
375
+ return {
376
+ "messages": messages,
377
+ "waveform": waveform,
378
+ "padded_waveform": padded_waveform,
379
+ "inputs": inputs,
380
+ "audio_duration_sec": audio_duration_sec,
381
+ "expected_tokens": expected_tokens,
382
+ }
383
+
384
+
385
+ def replace_special_tokens(
386
+ token_ids,
387
+ token_embeds,
388
+ modality_embeds,
389
+ special_token_id: int,
390
+ modality_name: str,
391
+ ):
392
+ positions = [idx for idx, token_id in enumerate(token_ids) if int(token_id) == int(special_token_id)]
393
+ if not positions:
394
  return token_embeds
395
 
396
+ flat_embeds = modality_embeds.reshape(-1, modality_embeds.shape[-1])
397
+ if len(positions) != flat_embeds.shape[0]:
398
  raise ValueError(
399
+ f"{modality_name.capitalize()} tokens and features do not match: "
400
+ f"tokens={len(positions)}, features={flat_embeds.shape[0]}"
401
  )
402
+ if token_embeds.shape[-1] != flat_embeds.shape[-1]:
403
  raise ValueError(
404
+ f"Embedding dim mismatch: token_dim={token_embeds.shape[-1]}, "
405
+ f"{modality_name}_dim={flat_embeds.shape[-1]}"
406
  )
407
+ token_embeds[positions, :] = flat_embeds
408
  return token_embeds
409
 
410
 
411
+ def replace_image_tokens(token_ids, token_embeds, image_embeds, image_token_id: int):
412
+ return replace_special_tokens(
413
+ token_ids,
414
+ token_embeds,
415
+ image_embeds,
416
+ special_token_id=image_token_id,
417
+ modality_name="image",
418
+ )
419
+
420
+
421
+ def replace_audio_tokens(token_ids, token_embeds, audio_embeds, audio_token_id: int):
422
+ return replace_special_tokens(
423
+ token_ids,
424
+ token_embeds,
425
+ audio_embeds,
426
+ special_token_id=audio_token_id,
427
+ modality_name="audio",
428
+ )
429
+
430
+
431
  def to_numpy_fp32(tensor_like) -> np.ndarray:
432
  if isinstance(tensor_like, np.ndarray):
433
  return tensor_like.astype(np.float32)
utils/infer_func.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import os
2
  import re
3
 
@@ -7,6 +8,23 @@ from ml_dtypes import bfloat16
7
  from tqdm import tqdm
8
 
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  def _layer_head_dim(config, layer_idx: int) -> int:
11
  if getattr(config, "layer_types", None) and layer_idx < len(config.layer_types):
12
  if config.layer_types[layer_idx] == "full_attention":
@@ -33,6 +51,28 @@ def _build_shared_kv_source_layers(config):
33
  return source_layers
34
 
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  def _find_axmodel_files(base_dir: str, expected_layers: int = None, expected_prefill: int = 128):
37
  files = os.listdir(base_dir)
38
  layer_pattern = re.compile(r"^(?P<prefix>.*)_p(?P<prefill>\d+)_l(?P<idx>\d+)_together\.axmodel$")
@@ -105,20 +145,38 @@ class InferManager:
105
  if post_file is None:
106
  raise FileNotFoundError("Cannot find post process .axmodel file in model_dir")
107
  self.post_process_session = InferenceSession(os.path.join(model_dir, post_file))
 
 
108
  print("Model loaded successfully!")
109
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  @staticmethod
111
  def _compute_mm_group_ids(mm_token_type_ids):
112
  if mm_token_type_ids is None:
113
  return None
114
 
115
  mm_token_type_ids = np.asarray(mm_token_type_ids, dtype=np.int32).reshape(-1)
116
- is_vision = np.isin(mm_token_type_ids, (1, 2))
117
- prev_is_vision = np.roll(is_vision, 1)
118
- prev_is_vision[0] = False
119
- new_group_starts = is_vision & ~prev_is_vision
120
  group_ids = np.cumsum(new_group_starts.astype(np.int32)) - 1
121
- group_ids[~is_vision] = -1
122
  return group_ids
123
 
124
  @staticmethod
@@ -136,7 +194,7 @@ class InferManager:
136
  if start // slice_len != end // slice_len:
137
  num_slices = end // slice_len - start // slice_len + 1
138
  print(
139
- f"[WARN] Image token block (group_id={group_id}, pos {start}-{end}) "
140
  f"spans {num_slices} prefill slices. Bidirectional attention within "
141
  f"earlier slices is partial (chunked prefill limitation)."
142
  )
@@ -340,7 +398,7 @@ class InferManager:
340
  if mm_token_type_ids is None:
341
  return True
342
  token_type = int(mm_token_type_ids[token_pos])
343
- return token_type not in (1, 2)
344
 
345
  @staticmethod
346
  def _build_decode_mask(cache_len: int, visible_past_tokens: int):
 
1
+ import atexit
2
  import os
3
  import re
4
 
 
8
  from tqdm import tqdm
9
 
10
 
11
+ def release_ax_inference_session(session):
12
+ inner = getattr(session, "_sess", None)
13
+ unload = getattr(inner, "_unload", None)
14
+ if not callable(unload):
15
+ return
16
+
17
+ try:
18
+ unload()
19
+ except Exception as exc:
20
+ print(f"[WARN] Failed to unload axengine session cleanly: {exc}")
21
+ finally:
22
+ try:
23
+ inner._unload = lambda: None
24
+ except Exception:
25
+ pass
26
+
27
+
28
  def _layer_head_dim(config, layer_idx: int) -> int:
29
  if getattr(config, "layer_types", None) and layer_idx < len(config.layer_types):
30
  if config.layer_types[layer_idx] == "full_attention":
 
51
  return source_layers
52
 
53
 
54
+ def detect_prefill_len(model_dir: str, default: int = 128) -> int:
55
+ """Auto-detect prefill_len (aka slice_len) from axmodel filenames.
56
+
57
+ Matches files named like ``<prefix>_p<N>_l<idx>_together.axmodel`` and returns
58
+ ``N``. Falls back to ``default`` when no layer files match.
59
+ """
60
+ layer_pattern = re.compile(r"^.*_p(?P<prefill>\d+)_l\d+_together\.axmodel$")
61
+ prefill_counts = {}
62
+ try:
63
+ for fname in os.listdir(model_dir):
64
+ match = layer_pattern.match(fname)
65
+ if match:
66
+ prefill = int(match.group("prefill"))
67
+ prefill_counts[prefill] = prefill_counts.get(prefill, 0) + 1
68
+ except FileNotFoundError:
69
+ return default
70
+
71
+ if not prefill_counts:
72
+ return default
73
+ return max(prefill_counts.items(), key=lambda kv: kv[1])[0]
74
+
75
+
76
  def _find_axmodel_files(base_dir: str, expected_layers: int = None, expected_prefill: int = 128):
77
  files = os.listdir(base_dir)
78
  layer_pattern = re.compile(r"^(?P<prefix>.*)_p(?P<prefill>\d+)_l(?P<idx>\d+)_together\.axmodel$")
 
145
  if post_file is None:
146
  raise FileNotFoundError("Cannot find post process .axmodel file in model_dir")
147
  self.post_process_session = InferenceSession(os.path.join(model_dir, post_file))
148
+ self._closed = False
149
+ atexit.register(self.close)
150
  print("Model loaded successfully!")
151
 
152
+ def close(self):
153
+ if self._closed:
154
+ return
155
+
156
+ sessions = list(getattr(self, "decoder_sessions", []))
157
+ post_process_session = getattr(self, "post_process_session", None)
158
+ if post_process_session is not None:
159
+ sessions.append(post_process_session)
160
+
161
+ for session in sessions:
162
+ release_ax_inference_session(session)
163
+
164
+ self.decoder_sessions = []
165
+ self.post_process_session = None
166
+ self._closed = True
167
+
168
  @staticmethod
169
  def _compute_mm_group_ids(mm_token_type_ids):
170
  if mm_token_type_ids is None:
171
  return None
172
 
173
  mm_token_type_ids = np.asarray(mm_token_type_ids, dtype=np.int32).reshape(-1)
174
+ is_multimodal = np.isin(mm_token_type_ids, (1, 2, 3))
175
+ prev_is_multimodal = np.roll(is_multimodal, 1)
176
+ prev_is_multimodal[0] = False
177
+ new_group_starts = is_multimodal & ~prev_is_multimodal
178
  group_ids = np.cumsum(new_group_starts.astype(np.int32)) - 1
179
+ group_ids[~is_multimodal] = -1
180
  return group_ids
181
 
182
  @staticmethod
 
194
  if start // slice_len != end // slice_len:
195
  num_slices = end // slice_len - start // slice_len + 1
196
  print(
197
+ f"[WARN] Multimodal token block (group_id={group_id}, pos {start}-{end}) "
198
  f"spans {num_slices} prefill slices. Bidirectional attention within "
199
  f"earlier slices is partial (chunked prefill limitation)."
200
  )
 
398
  if mm_token_type_ids is None:
399
  return True
400
  token_type = int(mm_token_type_ids[token_pos])
401
+ return token_type not in (1, 2, 3)
402
 
403
  @staticmethod
404
  def _build_decode_mask(cache_len: int, visible_past_tokens: int):