Skip to content

Commit ec8f099

Browse files
Qualcomm AI Engine Direct - LLM multi-batch quantization and evaluation (#20488)
### Summary - Support multi-batch calibration - Support multi-batch evaluation - Add CLI flag to specify batch size during calibration - Fix the shape of attention mask (Optimize runtime graph) ### Details about fix the shape of attention mask (Optimize runtime graph) The attention mask without the head dimension, the model had to broadcast the shape from [1, 1, S] to [1, 1, 1, S]. This introduced an extra `view_copy` node in the runtime graph, which is unnecessary. <img width="1265" height="693" alt="image" src="https://github.com/user-attachments/assets/76e382b5-739e-4cd7-a874-65cbb0a426ca" /> With head dim, no broadcast needed, redundant `view_copy` node removed <img width="1702" height="825" alt="image" src="https://github.com/user-attachments/assets/94df39be-4cc1-4a06-bd6b-c409fdd06580" /> ### Test plan - ExampleLLMScript - TestExampleMultimodalityScript
1 parent a5455c4 commit ec8f099

9 files changed

Lines changed: 71 additions & 17 deletions

File tree

Binary file not shown.

examples/qualcomm/oss_scripts/llama/dataset/builders.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ def build_calib_dataloaders(self) -> Dict[str, Optional[DataLoader]]:
265265
if len(decoder_datasets) > 1
266266
else decoder_datasets[0]
267267
)
268+
self._log_dataset_stats(datasets, cfg.batch_size, phase="calibration")
268269

269270
return dict.fromkeys(_ALL_MODALITY_KEYS) | {
270271
modality: DataLoader(
@@ -317,3 +318,31 @@ def build_runtime_dataloader(
317318
)
318319
for modality, dataset in datasets.items()
319320
}
321+
322+
@staticmethod
323+
def _log_dataset_stats(
324+
datasets: Dict[str, Dataset],
325+
batch_size: int,
326+
phase: str = "calibration",
327+
) -> None:
328+
"""Log sample/batch counts per modality; raises if any dataset < batch_size."""
329+
for modality, ds in datasets.items():
330+
n = len(ds)
331+
n_batches = n // batch_size
332+
dropped = n - n_batches * batch_size
333+
drop_str = f" ({dropped} dropped)" if batch_size > 1 and dropped else ""
334+
logging.info(
335+
"%s '%s': %d samples, batch_size=%d, %d batches%s",
336+
phase,
337+
modality,
338+
n,
339+
batch_size,
340+
n_batches,
341+
drop_str,
342+
)
343+
if batch_size > 1 and n < batch_size:
344+
raise ValueError(
345+
f"{phase} '{modality}' has {n} samples but "
346+
f"batch_size={batch_size}. "
347+
"Increase the data limit or reduce the batch size."
348+
)

examples/qualcomm/oss_scripts/llama/evaluator/lm_eval_adapter.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import Callable, Optional, Union
99

1010
import torch
11+
import torch.nn.functional as F
1112
from executorch.examples.models.llama.evaluate.eager_eval import EagerEvalWrapper
1213
from executorch.examples.qualcomm.oss_scripts.llama.inference import DecoderInference
1314
from pytorch_tokenizers.hf_tokenizer import HuggingFaceTokenizer
@@ -34,6 +35,7 @@ def __init__(
3435
max_seq_length: int,
3536
get_example_inputs: Callable,
3637
use_i64_token: bool,
38+
max_batch_size: int = 1,
3739
):
3840
assert max_seq_length is not None, "max_seq_length must be provided"
3941
super().__init__(
@@ -43,15 +45,23 @@ def __init__(
4345
self._runner = DecoderInference(
4446
get_example_inputs=get_example_inputs,
4547
max_context_len=max_seq_length,
48+
max_batch_size=max_batch_size,
4649
use_i64_token=use_i64_token,
4750
)
51+
self._batch_size = max_batch_size
52+
53+
@property
54+
def batch_size(self):
55+
return self._batch_size
4856

4957
def _model_call(self, inps):
58+
actual_bsz = inps.shape[0]
59+
inps = F.pad(inps, (0, 0, 0, self._batch_size - actual_bsz))
5060
logits = self._runner.predict_step(
5161
self._model,
5262
input_ids=inps,
5363
)
54-
return logits
64+
return logits[:actual_bsz]
5565

5666

5767
def run_lm_eval(
@@ -74,6 +84,7 @@ def run_lm_eval(
7484
max_seq_length=max_seq_length,
7585
get_example_inputs=get_example_inputs,
7686
use_i64_token=use_i64_token,
87+
max_batch_size=max_batch_size,
7788
)
7889
with torch.no_grad():
7990
eval_results = simple_evaluate(

examples/qualcomm/oss_scripts/llama/llama.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,15 @@ def _build_parser():
620620
"Multiple files are merged.",
621621
)
622622

623+
parser.add_argument(
624+
"--batch_size",
625+
type=int,
626+
default=1,
627+
help="Batch size for text decoder quantization. Larger values increase throughput "
628+
"but require more host memory. Only affects the CALIBRATE graph; DECODE and "
629+
"PREFILL graphs always use batch size 1.",
630+
)
631+
623632
parser.add_argument(
624633
"-F",
625634
"--use_fp16",

examples/qualcomm/oss_scripts/llama/masking_utils.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ def create_causal_attn_mask(max_batch_size: int, ar_len: int, max_context_len: i
3535
],
3636
dim=-1,
3737
)
38-
mask = mask[None, :, :].expand(max_batch_size, ar_len, max_context_len)
38+
# num_heads=1: the mask broadcasts across all heads.
39+
mask = mask[None, None, :, :].expand(max_batch_size, 1, ar_len, max_context_len)
3940
return mask
4041

4142

@@ -68,7 +69,8 @@ def create_sliding_window_attn_mask(
6869
],
6970
dim=-1,
7071
)
71-
mask = mask[None, :, :].expand(max_batch_size, ar_len, max_context_len)
72+
# num_heads=1: the mask broadcasts across all heads.
73+
mask = mask[None, None, :, :].expand(max_batch_size, 1, ar_len, max_context_len)
7274
return mask
7375

7476

@@ -123,8 +125,8 @@ def _mask_padding_positions(
123125
) -> None:
124126
"""Mask positions beyond each sequence's actual length."""
125127
actual_lens = torch.tensor([len(seq) for seq in input_ids])
126-
pad_rows = torch.arange(max_seq_length).unsqueeze(0) >= actual_lens.unsqueeze(1)
127-
self.mask.masked_fill_(pad_rows.unsqueeze(-1), PADDING_MASK_VALUE)
128+
pad_rows = torch.arange(max_seq_length) >= actual_lens.unsqueeze(1)
129+
self.mask.masked_fill_(pad_rows[:, None, :, None], PADDING_MASK_VALUE)
128130

129131

130132
class CausalAttentionMask(BaseAttentionMask):

examples/qualcomm/oss_scripts/llama/runner/multimodal_runner/multimodal_runner.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -247,10 +247,10 @@ Error QNNMultimodalRunner::load() {
247247
int32_t token_generator_ar_len = 0;
248248
int32_t max_cache_len = 0;
249249
int32_t max_ar_len = 0;
250-
// atten mask: [1, AR-N, CL]
250+
// atten mask: [1, 1, AR-N, CL]
251251
auto atten_mask_meta_token = method_meta->input_tensor_meta(1);
252-
token_generator_ar_len = atten_mask_meta_token->sizes()[1];
253-
context_len_ = atten_mask_meta_token->sizes()[2];
252+
token_generator_ar_len = atten_mask_meta_token->sizes()[2];
253+
context_len_ = atten_mask_meta_token->sizes()[3];
254254
if (eval_mode_ == EvalMode::kKVCached) {
255255
prompt_processor_ar_len = token_generator_ar_len;
256256
} else if (
@@ -259,7 +259,7 @@ Error QNNMultimodalRunner::load() {
259259
auto atten_mask_meta_prompt =
260260
text_decoder_->method_meta(prompt_processor_method_name)
261261
->input_tensor_meta(1);
262-
prompt_processor_ar_len = atten_mask_meta_prompt->sizes()[1];
262+
prompt_processor_ar_len = atten_mask_meta_prompt->sizes()[2];
263263
}
264264
if (prompt_processor_ar_len == context_len_)
265265
max_cache_len = context_len_;

examples/qualcomm/oss_scripts/llama/runner/runner.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,10 @@ Error Runner::load() {
244244
int32_t token_generator_ar_len = 0;
245245
int32_t max_cache_len = 0;
246246
int32_t max_ar_len = 0;
247-
// atten mask: [1, AR-N, CL]
247+
// atten mask: [1, 1, AR-N, CL]
248248
auto atten_mask_meta_token = method_meta->input_tensor_meta(1);
249-
token_generator_ar_len = atten_mask_meta_token->sizes()[1];
250-
context_len_ = atten_mask_meta_token->sizes()[2];
249+
token_generator_ar_len = atten_mask_meta_token->sizes()[2];
250+
context_len_ = atten_mask_meta_token->sizes()[3];
251251
if (eval_mode_ == EvalMode::kKVCached) {
252252
prompt_processor_ar_len = token_generator_ar_len;
253253
} else if (
@@ -256,7 +256,7 @@ Error Runner::load() {
256256
auto atten_mask_meta_prompt =
257257
module_->method_meta(prompt_processor_method_name)
258258
->input_tensor_meta(1);
259-
prompt_processor_ar_len = atten_mask_meta_prompt->sizes()[1];
259+
prompt_processor_ar_len = atten_mask_meta_prompt->sizes()[2];
260260
}
261261
if (prompt_processor_ar_len == context_len_)
262262
max_cache_len = context_len_;

examples/qualcomm/oss_scripts/llama/wrappers/base_component.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ def process_model_args(
9090
config: LLMModelConfig object to be used.
9191
mode: Mode of operation (PREFILL, DECODE, or CALIBRATE).
9292
"""
93-
# TODO: support batch inputs if necessary
9493
if mode == Mode.DECODE:
9594
ar_len = (
9695
# To get better performance, we round up to the nearest power of 2.
@@ -107,8 +106,8 @@ def process_model_args(
107106
else:
108107
raise ValueError(f"Unsupported mode: {mode}")
109108

110-
# TODO: support multi_batch for CALIBRATION MODE
111-
model_args.max_batch_size = 1
109+
# TODO: support batch inputs for runtime mode if necessary
110+
model_args.max_batch_size = control_args.batch_size if mode == Mode.CALIBRATE else 1
112111
model_args.max_seq_len = control_args.max_seq_len
113112
model_args.max_context_len = control_args.max_context_len
114113
model_args.use_kv_cache = (

examples/qualcomm/oss_scripts/llama/wrappers/llm_wrappers.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ def _tag_ios(self, node, fixed_point_type):
456456
atten_mask_shape = {
457457
(
458458
self.meta["get_max_batch_size"],
459+
1, # num_heads=1: the mask broadcasts across all heads.
459460
self.meta["get_ar_len"],
460461
self.meta["get_max_context_len"],
461462
),
@@ -609,6 +610,7 @@ def quantize(self, request: Request): # noqa: C901
609610
use_i64_token=self.control_args.embedding_quantize is not None,
610611
num_fewshot=self.control_args.eval_num_fewshot,
611612
limit=self.control_args.eval_limit,
613+
max_batch_size=self.meta["get_max_batch_size"],
612614
event_name="export_tasks",
613615
)
614616

@@ -674,6 +676,7 @@ def quantize(self, request: Request): # noqa: C901
674676
use_i64_token=self.control_args.embedding_quantize is not None,
675677
num_fewshot=self.control_args.eval_num_fewshot,
676678
limit=self.control_args.eval_limit,
679+
max_batch_size=self.meta["get_max_batch_size"],
677680
event_name="convert_pt2e_tasks",
678681
)
679682

@@ -1186,8 +1189,9 @@ def _calibrate(self, model, calibration_datasets):
11861189
outputs.append(outputs_each_batch)
11871190
return DataLoader(
11881191
ModalityEncoderDataset(outputs),
1189-
batch_size=1,
1192+
batch_size=self.control_args.batch_size,
11901193
shuffle=False,
1194+
drop_last=self.control_args.batch_size > 1,
11911195
)
11921196

11931197
def quantize(self, request: Request):

0 commit comments

Comments
 (0)