Skip to content

Commit 55e59c3

Browse files
committed
feat(cpu): add Qwen3.5 4B multimodal support
1 parent b9b46ae commit 55e59c3

7 files changed

Lines changed: 342 additions & 57 deletions

File tree

examples/qwen3_5/README.md

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
This example supports the text towers of
44
[`Qwen/Qwen3.5-0.8B`](https://huggingface.co/Qwen/Qwen3.5-0.8B) and
5-
[`Qwen/Qwen3.5-4B`](https://huggingface.co/Qwen/Qwen3.5-4B), plus single-image,
6-
ordered multi-image, and bounded short-video inference for Qwen3.5-0.8B.
5+
[`Qwen/Qwen3.5-4B`](https://huggingface.co/Qwen/Qwen3.5-4B), including
6+
single-image, ordered multi-image, and bounded short-video inference for both
7+
model sizes.
78

89
| Model | Text | Images | Decoded RGB-frame API | Local H.264 MP4 | Hidden size | GDN / full-attention layers |
910
| --- | --- | --- | --- | --- | ---: | ---: |
1011
| 0.8B | Yes | Yes | Yes | Yes, portable backend only | 1024 | 18 / 6 |
11-
| 4B | Yes | No | No | No | 2560 | 24 / 8 |
12+
| 4B | Yes | Yes | Yes | Yes, portable backend only | 2560 | 24 / 8 |
1213

1314
All configurations support batch size 1 and a maximum cache length of 2048
1415
tokens. `Qwen3_5ForCausalLM::resetState()` clears the full-attention KV cache
@@ -112,7 +113,7 @@ python examples/qwen3_5/validate_converted_model.py \
112113
--model-name Qwen3.5-0.8B
113114
```
114115

115-
### Qwen3.5-0.8B single-image
116+
### Qwen3.5-0.8B multimodal
116117

117118
```bash
118119
python examples/qwen3_5/validate_checkpoint.py \
@@ -163,6 +164,33 @@ python examples/qwen3_5/validate_converted_model.py \
163164
--model-name Qwen3.5-4B
164165
```
165166

167+
### Qwen3.5-4B multimodal
168+
169+
```bash
170+
python examples/qwen3_5/validate_checkpoint.py \
171+
/path/to/Qwen3.5-4B \
172+
--quant-config examples/qwen3_5/quant_cfg_4B_multimodal_w4a32_kai.json \
173+
--runtime-config examples/qwen3_5/config_4B_multimodal_w4a32_kai.json
174+
175+
python -m pymllm.mobile.utils.mllm_convertor \
176+
--input_path /path/to/Qwen3.5-4B \
177+
--output_path /path/to/qwen3.5-4b-multimodal-w4a32-kai.mllm \
178+
--model_name Qwen3.5-4B-Multimodal \
179+
--cfg_path examples/qwen3_5/quant_cfg_4B_multimodal_w4a32_kai.json \
180+
--pipeline w4a32_kai_pipeline \
181+
--include_prefix model.language_model. \
182+
--include_prefix model.visual. \
183+
--format v2 \
184+
--verbose
185+
186+
python examples/qwen3_5/validate_converted_model.py \
187+
/path/to/qwen3.5-4b-multimodal-w4a32-kai.mllm \
188+
/path/to/Qwen3.5-4B \
189+
--quant-config examples/qwen3_5/quant_cfg_4B_multimodal_w4a32_kai.json \
190+
--runtime-config examples/qwen3_5/config_4B_multimodal_w4a32_kai.json \
191+
--model-name Qwen3.5-4B-Multimodal
192+
```
193+
166194
The 4B converted tensor data exceeds 4 GiB. Use model-file V2 so descriptor
167195
sizes and offsets remain 64-bit, and provide at least 32 GiB of available host
168196
memory plus working disk space for conversion.
@@ -209,12 +237,23 @@ mllm-qwen3-5-runner \
209237
--config_path examples/qwen3_5/config_4B_w4a32_kai.json \
210238
--prompt "Give a one-sentence introduction." \
211239
--max_new_tokens 32
240+
241+
# Qwen3.5-4B multimodal; repeat --image_path for ordered multi-image input,
242+
# or replace it with --video_path in a decoder-enabled build.
243+
mllm-qwen3-5-runner \
244+
--model_path /path/to/qwen3.5-4b-multimodal-w4a32-kai.mllm \
245+
--model_version v2 \
246+
--tokenizer_path /path/to/Qwen3.5-4B/tokenizer.json \
247+
--config_path examples/qwen3_5/config_4B_multimodal_w4a32_kai.json \
248+
--image_path /path/to/image.jpg \
249+
--prompt "Describe the image." \
250+
--max_new_tokens 32
212251
```
213252

214253
Omit `--prompt` for the interactive loop. Repeat `--image_path` to attach
215254
multiple still images in order; the same ordered image list is used for each
216255
independent prompt in that process. Omit `--image_path` to run text-only
217-
inference with either the text-only or multimodal 0.8B model. `--video_path`
256+
inference with either the text-only or multimodal model of the selected size. `--video_path`
218257
requires a decoder-enabled build and cannot be combined with `--image_path`.
219258
Use `--video_fps`, `--video_max_frames`, `--video_max_bytes`,
220259
`--video_max_tokens`, `--video_max_decoded_pixels`, and
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
{
2+
"architectures": [
3+
"Qwen3_5ForConditionalGeneration"
4+
],
5+
"model_type": "qwen3_5",
6+
"image_token_id": 248056,
7+
"video_token_id": 248057,
8+
"vision_start_token_id": 248053,
9+
"vision_end_token_id": 248054,
10+
"image_min_pixels": 65536,
11+
"image_max_pixels": 262144,
12+
"text_config": {
13+
"attention_bias": false,
14+
"attn_output_gate": true,
15+
"eos_token_id": 248044,
16+
"full_attention_interval": 4,
17+
"head_dim": 256,
18+
"hidden_act": "silu",
19+
"hidden_size": 2560,
20+
"intermediate_size": 9216,
21+
"layer_types": [
22+
"linear_attention",
23+
"linear_attention",
24+
"linear_attention",
25+
"full_attention",
26+
"linear_attention",
27+
"linear_attention",
28+
"linear_attention",
29+
"full_attention",
30+
"linear_attention",
31+
"linear_attention",
32+
"linear_attention",
33+
"full_attention",
34+
"linear_attention",
35+
"linear_attention",
36+
"linear_attention",
37+
"full_attention",
38+
"linear_attention",
39+
"linear_attention",
40+
"linear_attention",
41+
"full_attention",
42+
"linear_attention",
43+
"linear_attention",
44+
"linear_attention",
45+
"full_attention",
46+
"linear_attention",
47+
"linear_attention",
48+
"linear_attention",
49+
"full_attention",
50+
"linear_attention",
51+
"linear_attention",
52+
"linear_attention",
53+
"full_attention"
54+
],
55+
"linear_conv_kernel_dim": 4,
56+
"linear_key_head_dim": 128,
57+
"linear_num_key_heads": 16,
58+
"linear_num_value_heads": 32,
59+
"linear_value_head_dim": 128,
60+
"mamba_ssm_dtype": "float32",
61+
"max_position_embeddings": 262144,
62+
"num_attention_heads": 16,
63+
"num_hidden_layers": 32,
64+
"num_key_value_heads": 4,
65+
"rms_norm_eps": 1e-06,
66+
"rope_parameters": {
67+
"mrope_interleaved": true,
68+
"mrope_section": [
69+
11,
70+
11,
71+
10
72+
],
73+
"partial_rotary_factor": 0.25,
74+
"rope_theta": 10000000,
75+
"rope_type": "default"
76+
},
77+
"tie_word_embeddings": true,
78+
"vocab_size": 248320
79+
},
80+
"vision_config": {
81+
"deepstack_visual_indexes": [],
82+
"depth": 24,
83+
"hidden_act": "gelu_pytorch_tanh",
84+
"hidden_size": 1024,
85+
"in_channels": 3,
86+
"intermediate_size": 4096,
87+
"num_heads": 16,
88+
"num_position_embeddings": 2304,
89+
"out_hidden_size": 2560,
90+
"patch_size": 16,
91+
"spatial_merge_size": 2,
92+
"temporal_patch_size": 2
93+
},
94+
"tie_word_embeddings": true,
95+
"max_cache_length": 2048,
96+
"linear_impl_type": "KaiLinear_f32_qai8dxp_qsi4c32p_mxk_nxk_qai8dxp1x8_qsi4c32p8x8_1x8x32"
97+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"^model\\.language_model\\.layers\\.\\d+\\.self_attn\\.q_proj\\.(bias|weight)": {"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [8192, 2560], "replace": true}},
3+
"^model\\.language_model\\.layers\\.\\d+\\.self_attn\\.k_proj\\.(bias|weight)": {"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [1024, 2560], "replace": true}},
4+
"^model\\.language_model\\.layers\\.\\d+\\.self_attn\\.v_proj\\.(bias|weight)": {"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [1024, 2560], "replace": true}},
5+
"^model\\.language_model\\.layers\\.\\d+\\.self_attn\\.o_proj\\.(bias|weight)": {"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [2560, 4096], "replace": true}},
6+
"^model\\.language_model\\.layers\\.\\d+\\.linear_attn\\.in_proj_qkv\\.(bias|weight)": {"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [8192, 2560], "replace": true}},
7+
"^model\\.language_model\\.layers\\.\\d+\\.linear_attn\\.in_proj_z\\.(bias|weight)": {"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [4096, 2560], "replace": true}},
8+
"^model\\.language_model\\.layers\\.\\d+\\.linear_attn\\.in_proj_a\\.(bias|weight)": {"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [32, 2560], "replace": true}},
9+
"^model\\.language_model\\.layers\\.\\d+\\.linear_attn\\.in_proj_b\\.(bias|weight)": {"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [32, 2560], "replace": true}},
10+
"^model\\.language_model\\.layers\\.\\d+\\.linear_attn\\.out_proj\\.(bias|weight)": {"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [2560, 4096], "replace": true}},
11+
"^model\\.language_model\\.layers\\.\\d+\\.mlp\\.gate_proj\\.(bias|weight)": {"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [9216, 2560], "replace": true}},
12+
"^model\\.language_model\\.layers\\.\\d+\\.mlp\\.up_proj\\.(bias|weight)": {"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [9216, 2560], "replace": true}},
13+
"^model\\.language_model\\.layers\\.\\d+\\.mlp\\.down_proj\\.(bias|weight)": {"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [2560, 9216], "replace": true}},
14+
"^model\\.language_model\\.embed_tokens\\.weight$": {"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [248320, 2560], "replace": false, "rename": "lm_head_out.weight"}},
15+
"^model\\.visual\\.blocks\\.\\d+\\.attn\\.qkv\\.(bias|weight)$": {"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [3072, 1024], "replace": true}},
16+
"^model\\.visual\\.blocks\\.\\d+\\.attn\\.proj\\.(bias|weight)$": {"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [1024, 1024], "replace": true}},
17+
"^model\\.visual\\.blocks\\.\\d+\\.mlp\\.linear_fc1\\.(bias|weight)$": {"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [4096, 1024], "replace": true}},
18+
"^model\\.visual\\.blocks\\.\\d+\\.mlp\\.linear_fc2\\.(bias|weight)$": {"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [1024, 4096], "replace": true}},
19+
"^model\\.visual\\.merger\\.linear_fc1\\.(bias|weight)$": {"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [4096, 4096], "replace": true}},
20+
"^model\\.visual\\.merger\\.linear_fc2\\.(bias|weight)$": {"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [2560, 4096], "replace": true}}
21+
}

examples/qwen3_5/test_validators.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,35 @@ def test_official_08b_multimodal_recipe_and_descriptors(self) -> None:
161161
103,
162162
)
163163

164+
def test_official_4b_multimodal_recipe_and_descriptors(self) -> None:
165+
config = _load_json("config_4B_multimodal_w4a32_kai.json")
166+
quant_config = _load_json("quant_cfg_4B_multimodal_w4a32_kai.json")
167+
168+
checkpoint_config = copy.deepcopy(config)
169+
checkpoint_config["vision_config"].update(
170+
{"initializer_range": 0.02, "model_type": "qwen3_5"}
171+
)
172+
validate_multimodal_config_contract(checkpoint_config, config)
173+
quantized_names, _ = validate_kai_recipe_contract(
174+
checkpoint_config["text_config"],
175+
quant_config,
176+
config,
177+
checkpoint_config["vision_config"],
178+
)
179+
self.assertEqual(len(quantized_names), 445)
180+
self.assertEqual(len(expected_vision_shapes(config["vision_config"])), 297)
181+
182+
expected = _expected_descriptors(config, quant_config)
183+
dtype_counts = Counter(dtype for dtype, _, _ in expected.values())
184+
self.assertEqual(len(expected), 626)
185+
self.assertEqual(dtype_counts[BYTE], 347)
186+
self.assertEqual(dtype_counts[FLOAT32], 279)
187+
self.assertEqual(
188+
sum(name.startswith("model.visual.") for name in expected),
189+
199,
190+
)
191+
self.assertEqual(sum(size for _, _, size in expected.values()), 5_126_041_600)
192+
164193
def test_multimodal_recipe_rejects_text_runtime_and_contract_drift(self) -> None:
165194
config = _load_json("config_0.8B_multimodal_w4a32_kai.json")
166195
text_runtime = _load_json("config_0.8B_w4a32_kai.json")
@@ -179,6 +208,17 @@ def test_multimodal_recipe_rejects_text_runtime_and_contract_drift(self) -> None
179208
with self.assertRaisesRegex(AssertionError, "image_max_pixels"):
180209
validate_multimodal_config_contract(config, mutated)
181210

211+
config_4b = _load_json("config_4B_multimodal_w4a32_kai.json")
212+
wrong_vision = copy.deepcopy(config_4b)
213+
wrong_vision["vision_config"]["depth"] = 12
214+
with self.assertRaisesRegex(
215+
AssertionError, "official Qwen3.5-4B"
216+
):
217+
validate_multimodal_config_contract(config_4b, wrong_vision)
218+
219+
with self.assertRaisesRegex(AssertionError, "runtime is Qwen3.5-0.8B"):
220+
validate_multimodal_config_contract(config_4b, config)
221+
182222
def test_variant_resolution_rejects_runtime_semantic_mismatches(self) -> None:
183223
text_config = _load_json("config_4B_w4a32_kai.json")["text_config"]
184224
mutations = (

0 commit comments

Comments
 (0)