Instructions to use AXERA-TECH/gemma-4-E2B-it with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use AXERA-TECH/gemma-4-E2B-it with Transformers:
# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("AXERA-TECH/gemma-4-E2B-it", device_map="auto") - Notebooks
- Google Colab
- Kaggle
yongqiang commited on
Commit ·
3a6e68f
1
Parent(s): 03f32f8
Add packaged audio inference support and refresh deployment assets
Browse files- .gitattributes +2 -0
- README.md +215 -117
- assets/gemma4_audio_test_5s.wav +3 -0
- assets/gemma4_audio_test_chunk0_30s.wav +3 -0
- assets/gemma4_audio_test_chunk1_30s.wav +3 -0
- assets/openai_api_demo.png +3 -0
- gemma4_audio_30s.axmodel +3 -0
- gemma4_audio_5s.axmodel +3 -0
- infer_axmodel.py +205 -24
- post_config.json +2 -2
- utils/gemma4_compat.py +1 -0
- utils/gemma4_multimodal.py +164 -16
- utils/infer_func.py +65 -7
.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
|
| 27 |
-
- Supports
|
| 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 `
|
| 46 |
-
- `w4a16`: TTFT is approximately `
|
| 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
|
| 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 |

|
| 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 |
-
|
| 148 |
-
|
|
|
|
| 149 |
tokenizer_type = 3
|
| 150 |
huggingface tokenizer mode = space_replace_bpe
|
| 151 |
-
31% | ########## | 12 / 38 [
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
100% | ################################ | 38 / 38 [
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
|
|
|
|
|
|
| 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
|
| 192 |
----------------------------------------
|
| 193 |
-
prompt >> who are you?
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
media >>
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
15:
|
| 209 |
-
15:
|
| 210 |
-
15:
|
| 211 |
-
15:
|
| 212 |
-
15:
|
| 213 |
-
15:
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
* **
|
| 221 |
-
* **
|
| 222 |
-
* **
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 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 |
-
|
| 245 |
-
|
|
|
|
| 246 |
tokenizer_type = 3
|
| 247 |
huggingface tokenizer mode = space_replace_bpe
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
100% | ################################ | 38 / 38 [
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
|
|
|
|
|
|
| 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 `--
|
| 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 |

|
| 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 |
+

|
| 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
|
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 |
-
|
| 69 |
-
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 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 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 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":
|
| 3 |
"temperature": 1.0,
|
| 4 |
"enable_repetition_penalty": false,
|
| 5 |
"repetition_penalty": 1.0,
|
| 6 |
"penalty_window": 64,
|
| 7 |
-
"enable_top_p_sampling":
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 265 |
-
|
| 266 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
return token_embeds
|
| 268 |
|
| 269 |
-
|
| 270 |
-
if len(
|
| 271 |
raise ValueError(
|
| 272 |
-
f"
|
| 273 |
-
f"features={
|
| 274 |
)
|
| 275 |
-
if token_embeds.shape[-1] !=
|
| 276 |
raise ValueError(
|
| 277 |
-
f"Embedding dim mismatch: token_dim={token_embeds.shape[-1]},
|
|
|
|
| 278 |
)
|
| 279 |
-
token_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 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
new_group_starts =
|
| 120 |
group_ids = np.cumsum(new_group_starts.astype(np.int32)) - 1
|
| 121 |
-
group_ids[~
|
| 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]
|
| 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):
|