Skip to content

Commit 769b0cf

Browse files
Qualcomm AI Engine Direct - Update mix-precision analyzer unitest (#20688)
### Summary - Update test_analyzer_to_file_generation to the new analyze(decoder_inference, text_dataloader) signature - Replace SimpleModel with SimpleLLMDecoder so the SQNR analyzer captures a realistic per-layer structure; add it to tests/models.py ### Test plan ``` bash python -m backends.qualcomm.tests.test_qnn_delegate TestUtilsScript.test_analyzer_to_file_generation --device $DEVICE --soc_model ${soc} --build_folder build-android --executorch_root . ```
1 parent 5bce981 commit 769b0cf

2 files changed

Lines changed: 104 additions & 9 deletions

File tree

backends/qualcomm/tests/models.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2397,6 +2397,77 @@ def forward(self, x, y):
23972397
return z5
23982398

23992399

2400+
class SimpleLLMDecoder(torch.nn.Module):
2401+
"""
2402+
Minimal transformer decoder mirroring how QNN LLM decoders are built:
2403+
a token embedding feeds a stack of decoder blocks whose linear projections
2404+
are expressed as 1x1 conv2d (see static_llama.py), grouped under a
2405+
``layers.N`` ModuleList. Takes token ids and an additive attention mask.
2406+
"""
2407+
2408+
class ConvAttention(torch.nn.Module):
2409+
def __init__(self, dim, n_heads):
2410+
super().__init__()
2411+
self.n_heads = n_heads
2412+
self.head_dim = dim // n_heads
2413+
self.scale = self.head_dim**-0.5
2414+
self.wq_conv = torch.nn.Conv2d(dim, dim, 1, bias=False)
2415+
self.wk_conv = torch.nn.Conv2d(dim, dim, 1, bias=False)
2416+
self.wv_conv = torch.nn.Conv2d(dim, dim, 1, bias=False)
2417+
self.wo_conv = torch.nn.Conv2d(dim, dim, 1, bias=False)
2418+
2419+
def forward(self, x, atten_mask): # x: (b, dim, 1, seq)
2420+
b, dim, _, seq = x.shape
2421+
q = self.wq_conv(x).view(b, self.n_heads, self.head_dim, seq)
2422+
k = self.wk_conv(x).view(b, self.n_heads, self.head_dim, seq)
2423+
v = self.wv_conv(x).view(b, self.n_heads, self.head_dim, seq)
2424+
attn = torch.matmul(q.transpose(-2, -1), k) * self.scale
2425+
attn = torch.softmax(attn + atten_mask, dim=-1)
2426+
ctx = torch.matmul(v, attn.transpose(-2, -1))
2427+
ctx = ctx.reshape(b, dim, 1, seq)
2428+
return self.wo_conv(ctx)
2429+
2430+
class ConvFeedForward(torch.nn.Module):
2431+
def __init__(self, dim, hidden_dim):
2432+
super().__init__()
2433+
self.w1_conv = torch.nn.Conv2d(dim, hidden_dim, 1, bias=False)
2434+
self.w2_conv = torch.nn.Conv2d(hidden_dim, dim, 1, bias=False)
2435+
self.w3_conv = torch.nn.Conv2d(dim, hidden_dim, 1, bias=False)
2436+
self.act_fn = torch.nn.SiLU()
2437+
2438+
def forward(self, x):
2439+
return self.w2_conv(self.act_fn(self.w1_conv(x)) * self.w3_conv(x))
2440+
2441+
class DecoderLayer(torch.nn.Module):
2442+
def __init__(self, dim, hidden_dim, n_heads):
2443+
super().__init__()
2444+
self.attention = SimpleLLMDecoder.ConvAttention(dim, n_heads)
2445+
self.feed_forward = SimpleLLMDecoder.ConvFeedForward(dim, hidden_dim)
2446+
2447+
def forward(self, x, atten_mask):
2448+
x = x + self.attention(x, atten_mask)
2449+
x = x + self.feed_forward(x)
2450+
return x
2451+
2452+
def __init__(self, vocab_size=128, dim=32, hidden_dim=64, n_heads=4, n_layers=1):
2453+
super().__init__()
2454+
self.tok_embeddings = torch.nn.Embedding(vocab_size, dim)
2455+
self.layers = torch.nn.ModuleList(
2456+
[self.DecoderLayer(dim, hidden_dim, n_heads) for _ in range(n_layers)]
2457+
)
2458+
self.output_conv = torch.nn.Conv2d(dim, dim, 1, bias=False)
2459+
self.eval()
2460+
2461+
def forward(self, input_ids, atten_mask): # input_ids: (b, seq)
2462+
x = self.tok_embeddings(input_ids) # (b, seq, dim)
2463+
b, seq, dim = x.shape
2464+
x = x.reshape(b, seq, 1, dim).transpose(1, 3) # (b, dim, 1, seq)
2465+
for layer in self.layers:
2466+
x = layer(x, atten_mask)
2467+
x = self.output_conv(x)
2468+
return x.transpose(1, 3).reshape(b, seq, dim)
2469+
2470+
24002471
class SkipBackToBack(torch.nn.Module):
24012472

24022473
def __init__(self):

backends/qualcomm/tests/test_qnn_delegate.py

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10881,19 +10881,40 @@ def test_analyzer_to_file_generation(self):
1088110881
save_suggest_recipes,
1088210882
)
1088310883

10884-
module = SimpleModel() # noqa: F405
10885-
sample_input = (torch.ones(1, 32, 28, 28), torch.ones(1, 32, 28, 28))
10884+
torch.manual_seed(8)
10885+
n_layers = 20
10886+
vocab_size, seq_len, n_heads = 128, 8, 4
10887+
module = SimpleLLMDecoder( # noqa: F405
10888+
vocab_size=vocab_size, n_heads=n_heads, n_layers=n_layers
10889+
)
10890+
input_ids = torch.randint(0, vocab_size, (1, seq_len), dtype=torch.int32)
10891+
atten_mask = torch.triu(
10892+
torch.full((1, 1, seq_len, seq_len), float("-inf")), diagonal=1
10893+
)
10894+
sample_input = (input_ids, atten_mask)
1088610895
fp32_gm = torch.export.export(module, sample_input, strict=True).module()
1088710896
qdq_gm = self.get_qdq_module(
1088810897
module, sample_input, quant_dtype=QuantDtype.use_8a4w
1088910898
)
1089010899

10900+
class DecoderInference:
10901+
def get_inputs(self, input_ids, attn_mask):
10902+
return (input_ids, attn_mask)
10903+
10904+
text_dataloader = [
10905+
{
10906+
"input_ids": input_ids,
10907+
"attention_mask": atten_mask,
10908+
}
10909+
]
10910+
10911+
num_sharding = 5
1089110912
report = PerLayerSqnrAnalyzer(
10892-
model_name="simple_conv",
10893-
num_layers=4,
10913+
model_name="simple_llm_decoder",
10914+
num_layers=n_layers,
1089410915
fp32_gm=fp32_gm,
1089510916
qdq_gm=qdq_gm,
10896-
).analyze([sample_input], num_sharding=4)
10917+
).analyze(DecoderInference(), text_dataloader, num_sharding=num_sharding)
1089710918

1089810919
overrides = report.suggest_recipe_overrides(sqnr_threshold=22.0)
1089910920

@@ -10902,10 +10923,13 @@ def test_analyzer_to_file_generation(self):
1090210923
save_suggest_recipes(report, overrides, output_dir=tmp_dir)
1090310924

1090410925
# --- save_analysis_summary csv file ---
10905-
with open(f"{tmp_dir}/simple_conv_quantization_error.csv") as f:
10926+
with open(f"{tmp_dir}/simple_llm_decoder_quantization_error.csv") as f:
1090610927
csv_content = f.read()
1090710928
rows = list(csv.reader(csv_content.splitlines()))
10908-
self.assertEqual(len(rows), 5) # 1 header + 4 group rows
10929+
# 1 header + per-shard conv groups (7 projections each: wq/wk/wv/wo,
10930+
# w1/w2/w3) + the model-level output_conv. Layers are bucketed into
10931+
# num_sharding contiguous shards (n_layers >= num_sharding).
10932+
self.assertEqual(len(rows), 1 + num_sharding * 7 + 1)
1090910933
self.assertEqual(
1091010934
rows[0],
1091110935
[
@@ -10921,11 +10945,11 @@ def test_analyzer_to_file_generation(self):
1092110945

1092210946
# --- save_suggest_recipes .py file (only written when sensitive layers exist) ---
1092310947
if overrides:
10924-
with open(f"{tmp_dir}/simple_conv_suggest_recipe.py") as f:
10948+
with open(f"{tmp_dir}/simple_llm_decoder_suggest_recipe.py") as f:
1092510949
py_content = f.read()
1092610950
# generated file must be valid Python
1092710951
try:
10928-
compile(py_content, "simple_conv_suggest_recipe.py", "exec")
10952+
compile(py_content, "simple_llm_decoder_suggest_recipe.py", "exec")
1092910953
except SyntaxError as e:
1093010954
self.fail(
1093110955
f"Generated recipe file has syntax error: {e}\n{py_content}"

0 commit comments

Comments
 (0)