Skip to content

Commit 27acb30

Browse files
Qualcomm AI Engine Direct - Support Multimodal VLMs (pytorch#16292)
Summary: - add support for multimodal AOT and calibration - AOT support for VLMs: - SmolVLM-500M - InternVL3-1B - add quantization recipe for VLMs and vision encoders - Refactor LLM AOT and integrated it into multimodal AOT ### Test plan Test lowering of the quantized SmolVLM-500M ``` bash python -m backends.qualcomm.tests.test_qnn_delegate TestExampleMultimodalityScript.test_smolvlm_500m_instruct -b build-android --executorch_root . -a . -m SM8750 ``` Test lowering of the quantized InternVL3-1B ``` bash python -m backends.qualcomm.tests.test_qnn_delegate TestExampleMultimodalityScript.test_internvl3_1b -b build-android --executorch_root . -a . -m SM8750 ```
1 parent 39203cf commit 27acb30

21 files changed

Lines changed: 3288 additions & 1078 deletions

‎backends/qualcomm/tests/test_qnn_delegate.py‎

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6516,6 +6516,126 @@ def test_qwen2_5(self):
65166516
)
65176517

65186518

6519+
class TestExampleMultimodalityScript(TestQNN):
6520+
def test_smolvlm_500m_instruct(self):
6521+
if not self.required_envs():
6522+
self.skipTest("missing required envs")
6523+
6524+
prompt = "Can you describe this image?"
6525+
cmds = [
6526+
"python",
6527+
f"{self.executorch_root}/examples/qualcomm/oss_scripts/llama/llama.py",
6528+
"--artifact",
6529+
self.artifact_dir,
6530+
"--build_folder",
6531+
self.build_folder,
6532+
"--model",
6533+
self.model,
6534+
"--ip",
6535+
self.ip,
6536+
"--port",
6537+
str(self.port),
6538+
"--prompt",
6539+
prompt,
6540+
"--temperature",
6541+
"0",
6542+
"--decoder_model",
6543+
"smolvlm_500m_instruct",
6544+
"--model_mode",
6545+
"kv",
6546+
"--max_seq_len",
6547+
"128",
6548+
]
6549+
if self.compile_only:
6550+
cmds.extend(["--compile_only"])
6551+
elif self.device:
6552+
cmds.extend(["--device", self.device])
6553+
if self.host:
6554+
cmds.extend(["--host", self.host])
6555+
elif self.enable_x86_64:
6556+
cmds.extend(["--enable_x86_64"])
6557+
if self.pre_gen_pte:
6558+
cmds.extend(["--pre_gen_pte", self.pre_gen_pte])
6559+
6560+
p = subprocess.Popen(cmds, stdout=subprocess.DEVNULL)
6561+
with Listener((self.ip, self.port)) as listener:
6562+
conn = listener.accept()
6563+
p.communicate()
6564+
msg = json.loads(conn.recv())
6565+
if "Error" in msg:
6566+
self.fail(msg["Error"])
6567+
else:
6568+
if not self.enable_x86_64:
6569+
encoder_pte_size = msg["encoder_pte_size"]
6570+
text_embedding_pte_size = msg["text_embedding_pte_size"]
6571+
decoder_pte_size = msg["pte_size"]
6572+
self.assertLessEqual(encoder_pte_size, 110_000_000) # 110MB
6573+
self.assertLessEqual(text_embedding_pte_size, 100_000_000) # 100MB
6574+
self.assertLessEqual(decoder_pte_size, 400_000_000) # 400MB
6575+
print(f"Encoder PTE Size: {encoder_pte_size} bytes")
6576+
print(f"Text Embedding PTE Size: {text_embedding_pte_size} bytes")
6577+
print(f"Decoder PTE Size: {decoder_pte_size} bytes")
6578+
6579+
def test_internvl3_1b(self):
6580+
if not self.required_envs():
6581+
self.skipTest("missing required envs")
6582+
6583+
prompt = "Can you describe this image?"
6584+
cmds = [
6585+
"python",
6586+
f"{self.executorch_root}/examples/qualcomm/oss_scripts/llama/llama.py",
6587+
"--artifact",
6588+
self.artifact_dir,
6589+
"--build_folder",
6590+
self.build_folder,
6591+
"--model",
6592+
self.model,
6593+
"--ip",
6594+
self.ip,
6595+
"--port",
6596+
str(self.port),
6597+
"--prompt",
6598+
prompt,
6599+
"--temperature",
6600+
"0",
6601+
"--decoder_model",
6602+
"internvl3_1b",
6603+
"--model_mode",
6604+
"kv",
6605+
"--max_seq_len",
6606+
"320",
6607+
]
6608+
if self.compile_only:
6609+
cmds.extend(["--compile_only"])
6610+
elif self.device:
6611+
cmds.extend(["--device", self.device])
6612+
if self.host:
6613+
cmds.extend(["--host", self.host])
6614+
elif self.enable_x86_64:
6615+
cmds.extend(["--enable_x86_64"])
6616+
if self.pre_gen_pte:
6617+
cmds.extend(["--pre_gen_pte", self.pre_gen_pte])
6618+
6619+
p = subprocess.Popen(cmds, stdout=subprocess.DEVNULL)
6620+
with Listener((self.ip, self.port)) as listener:
6621+
conn = listener.accept()
6622+
p.communicate()
6623+
msg = json.loads(conn.recv())
6624+
if "Error" in msg:
6625+
self.fail(msg["Error"])
6626+
else:
6627+
if not self.enable_x86_64:
6628+
encoder_pte_size = msg["encoder_pte_size"]
6629+
text_embedding_pte_size = msg["text_embedding_pte_size"]
6630+
decoder_pte_size = msg["pte_size"]
6631+
self.assertLessEqual(encoder_pte_size, 425_000_000) # 425MB
6632+
self.assertLessEqual(text_embedding_pte_size, 300_000_000) # 300MB
6633+
self.assertLessEqual(decoder_pte_size, 550_000_000) # 550MB
6634+
print(f"Encoder PTE Size: {encoder_pte_size} bytes")
6635+
print(f"Text Embedding PTE Size: {text_embedding_pte_size} bytes")
6636+
print(f"Decoder PTE Size: {decoder_pte_size} bytes")
6637+
6638+
65196639
class TestExampleOssScript(TestQNN):
65206640
def test_albert(self):
65216641
if not self.required_envs([self.sentence_dataset]):
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"dim": 896,
3+
"ffn_dim_multiplier": 1,
4+
"hidden_dim": 4864,
5+
"n_heads": 14,
6+
"n_kv_heads": 2,
7+
"n_layers": 24,
8+
"norm_eps": 1e-06,
9+
"rope_theta": 1000000.0,
10+
"use_scaled_rope": false,
11+
"vocab_size": 151674,
12+
"use_hf_rope": true,
13+
"attention_qkv_bias": true
14+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# This source code is licensed under the BSD-style license found in the
2+
# LICENSE file in the root directory of this source tree.
3+
4+
from executorch.examples.models.internvl3.convert_weights import convert_weights
5+
from executorch.examples.models.llama.model import Llama2Model
6+
7+
8+
class InternVL3Model(Llama2Model):
9+
def __init__(self, **kwargs):
10+
super().__init__(**kwargs)
11+
12+
13+
__all__ = [
14+
"InternVL3Model",
15+
"convert_weights",
16+
]
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import argparse
2+
from typing import Dict
3+
4+
import torch
5+
6+
from executorch.examples.models.smollm3.convert_weights import load_checkpoint
7+
from torchtune.models.convert_weights import get_mapped_key
8+
9+
# Standard _FROM_META weight mapping of Meta weights to TorchTune + additional bias weight mappings.
10+
_INTERNVL_TO_META = {
11+
"language_model.model.embed_tokens.weight": "tok_embeddings.weight",
12+
"language_model.model.norm.weight": "norm.weight",
13+
"language_model.model.layers.{}.self_attn.k_proj.weight": "layers.{}.attention.wk.weight",
14+
"language_model.model.layers.{}.self_attn.k_proj.bias": "layers.{}.attention.wk.bias",
15+
"language_model.model.layers.{}.self_attn.q_proj.weight": "layers.{}.attention.wq.weight",
16+
"language_model.model.layers.{}.self_attn.q_proj.bias": "layers.{}.attention.wq.bias",
17+
"language_model.model.layers.{}.self_attn.v_proj.weight": "layers.{}.attention.wv.weight",
18+
"language_model.model.layers.{}.self_attn.v_proj.bias": "layers.{}.attention.wv.bias",
19+
"language_model.model.layers.{}.self_attn.o_proj.weight": "layers.{}.attention.wo.weight",
20+
"language_model.model.layers.{}.input_layernorm.weight": "layers.{}.attention_norm.weight",
21+
"language_model.model.layers.{}.post_attention_layernorm.weight": "layers.{}.ffn_norm.weight",
22+
"language_model.model.layers.{}.mlp.gate_proj.weight": "layers.{}.feed_forward.w1.weight",
23+
"language_model.model.layers.{}.mlp.down_proj.weight": "layers.{}.feed_forward.w2.weight",
24+
"language_model.model.layers.{}.mlp.up_proj.weight": "layers.{}.feed_forward.w3.weight",
25+
}
26+
27+
28+
def internvl_tune_to_meta(
29+
state_dict: Dict[str, torch.Tensor]
30+
) -> Dict[str, torch.Tensor]:
31+
"""
32+
Convert a state dict from torchtune's format to Meta's format. This function
33+
doesn't handle any sharding or splitting of state dicts. It follows the
34+
state_dict IN -> state_dict OUT pattern.
35+
36+
Args:
37+
state_dict (Dict[str, torch.Tensor]): State dict in torchtune's format.
38+
39+
Returns:
40+
Dict[str, torch.Tensor]: State dict in Meta's format.
41+
"""
42+
converted_text_model_state_dict = {}
43+
for key, value in state_dict.items():
44+
try:
45+
new_key = get_mapped_key(key, _INTERNVL_TO_META)
46+
converted_text_model_state_dict[new_key] = value
47+
except:
48+
# only preserve parameters of text decoder
49+
pass
50+
converted_text_model_state_dict["output.weight"] = state_dict[
51+
"language_model.lm_head.weight"
52+
]
53+
return converted_text_model_state_dict
54+
55+
56+
def convert_weights(input_dir: str, output_file: str) -> None:
57+
print("Loading checkpoint...")
58+
sd = load_checkpoint(input_dir)
59+
print("Converting checkpoint...")
60+
converted_sd = internvl_tune_to_meta(sd)
61+
print("Saving checkpoint...")
62+
torch.save(converted_sd, output_file)
63+
print("Done.")
64+
65+
66+
def main():
67+
parser = argparse.ArgumentParser(
68+
description="Convert InternVL3 weights to Meta format."
69+
)
70+
parser.add_argument(
71+
"input_dir",
72+
type=str,
73+
help="Path to directory containing checkpoint files",
74+
)
75+
parser.add_argument("output", type=str, help="Path to the output checkpoint")
76+
77+
args = parser.parse_args()
78+
convert_weights(args.input_dir, args.output)
79+
80+
81+
if __name__ == "__main__":
82+
main()
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"dim": 960,
3+
"ffn_dim_multiplier": 1,
4+
"hidden_dim": 2560,
5+
"n_heads": 15,
6+
"n_kv_heads": 5,
7+
"n_layers": 32,
8+
"norm_eps": 1e-05,
9+
"rope_theta": 100000,
10+
"use_scaled_rope": false,
11+
"vocab_size": 49280,
12+
"use_hf_rope": false,
13+
"attention_qkv_bias": false
14+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# This source code is licensed under the BSD-style license found in the
2+
# LICENSE file in the root directory of this source tree.
3+
4+
from executorch.examples.models.llama.model import Llama2Model
5+
from executorch.examples.models.smolvlm.convert_weights import convert_weights
6+
7+
8+
class SmolVLMModel(Llama2Model):
9+
def __init__(self, **kwargs):
10+
super().__init__(**kwargs)
11+
12+
13+
__all__ = [
14+
"SmolVLMModel",
15+
"convert_weights",
16+
]
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import argparse
2+
from typing import Dict
3+
4+
import torch
5+
6+
from executorch.examples.models.smollm3.convert_weights import load_checkpoint
7+
from torchtune.models.convert_weights import get_mapped_key
8+
9+
# Standard _FROM_META weight mapping of Meta weights to TorchTune + additional bias weight mappings.
10+
_SMOLVLM_TO_META = {
11+
"model.text_model.embed_tokens.weight": "tok_embeddings.weight",
12+
"model.text_model.norm.weight": "norm.weight",
13+
"model.text_model.layers.{}.self_attn.k_proj.weight": "layers.{}.attention.wk.weight",
14+
"model.text_model.layers.{}.self_attn.q_proj.weight": "layers.{}.attention.wq.weight",
15+
"model.text_model.layers.{}.self_attn.v_proj.weight": "layers.{}.attention.wv.weight",
16+
"model.text_model.layers.{}.self_attn.o_proj.weight": "layers.{}.attention.wo.weight",
17+
"model.text_model.layers.{}.input_layernorm.weight": "layers.{}.attention_norm.weight",
18+
"model.text_model.layers.{}.post_attention_layernorm.weight": "layers.{}.ffn_norm.weight",
19+
"model.text_model.layers.{}.mlp.gate_proj.weight": "layers.{}.feed_forward.w1.weight",
20+
"model.text_model.layers.{}.mlp.down_proj.weight": "layers.{}.feed_forward.w2.weight",
21+
"model.text_model.layers.{}.mlp.up_proj.weight": "layers.{}.feed_forward.w3.weight",
22+
}
23+
24+
25+
def smolvlm_tune_to_meta(
26+
state_dict: Dict[str, torch.Tensor]
27+
) -> Dict[str, torch.Tensor]:
28+
"""
29+
Convert a state dict from torchtune's format to Meta's format. This function
30+
doesn't handle any sharding or splitting of state dicts. It follows the
31+
state_dict IN -> state_dict OUT pattern.
32+
33+
Args:
34+
state_dict (Dict[str, torch.Tensor]): State dict in torchtune's format.
35+
36+
Returns:
37+
Dict[str, torch.Tensor]: State dict in Meta's format.
38+
"""
39+
converted_text_model_state_dict = {}
40+
for key, value in state_dict.items():
41+
try:
42+
new_key = get_mapped_key(key, _SMOLVLM_TO_META)
43+
converted_text_model_state_dict[new_key] = value
44+
except:
45+
# only preserve parameters of text decoder
46+
pass
47+
converted_text_model_state_dict["output.weight"] = state_dict["lm_head.weight"]
48+
return converted_text_model_state_dict
49+
50+
51+
def convert_weights(input_dir: str, output_file: str) -> None:
52+
print("Loading checkpoint...")
53+
sd = load_checkpoint(input_dir)
54+
print("Converting checkpoint...")
55+
converted_sd = smolvlm_tune_to_meta(sd)
56+
print("Saving checkpoint...")
57+
torch.save(converted_sd, output_file)
58+
print("Done.")
59+
60+
61+
def main():
62+
parser = argparse.ArgumentParser(
63+
description="Convert SmolVLM weights to Meta format."
64+
)
65+
parser.add_argument(
66+
"input_dir",
67+
type=str,
68+
help="Path to directory containing checkpoint files",
69+
)
70+
parser.add_argument("output", type=str, help="Path to the output checkpoint")
71+
72+
args = parser.parse_args()
73+
convert_weights(args.input_dir, args.output)
74+
75+
76+
if __name__ == "__main__":
77+
main()

‎examples/qualcomm/oss_scripts/llama/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ This file provides you the instructions to run LLM Decoder model with different
1919
1. QWEN3 0.6B / 1.7B
2020
1. SmolLM2 135M
2121
1. SmolLM3 3B
22-
22+
2323

2424
We offer the following modes to execute the model:
2525

0 commit comments

Comments
 (0)