Skip to content

Commit f3d2077

Browse files
Qualcomm AI Engine Direct - Decouple quantization and compile graphs for faster VLM/LLM PTQ (pytorch#19220)
### Summary - Calibrate decoder using prefill stage only (full chunk tokens) - Remove need for AR-N calibration loops - Significantly reduce calibration overhead | model name | before</br> Time(sec) | after</br> Time(sec) | speedup | |------------|--------------|-------------|---------| | gemma-2b | 1216 | 259 | 4.69x | | gemma2-2b | 1827 | 382 | 4.78x | | gemma3-1b | 907 | 218 | 4.16x | | glm-1_5b | 963 | 167 | 5.76x | | granite_3_3-2b | 1545 | 304 | 5.08x | | llama3_2-1b | 1237 | 285 | 4.34× | | llama3_2-3b | 2286 | 813 | 2.81x | | phi_4_mini | 2824 | 363 | 7.77x | | qwen2_5-0_5b | 486 | 119 | 4.08x | | qwen2_5-1_5b | 1068 | 220 | 4.86× | | qwen3-0_6b | 1013 | 158 | 6.41× | | qwen3-1_7b | 1478 | 283 | 5.22× | | smollm2_135m | 399 | 122 | 3.27× | | smollm3-3b | 2065 | 431 | 4.79x | | smolvlm_500m_instruct | 170 | 131 | 1.30× | | internvl3_1b | 170 | 103 | 1.65x | | granite_speech_3_3-2b | 447 | 215 | 2.07x | This change decouples the quantization graph from the graph used for subsequent lowering, so calibration no longer depends on the AR-N decoding flow. Previously, we were running calibration directly on the graph shaped for lowering (with fixed AR-N constraints). That forced us into an autoregressive loop (AR1 per step), which was both inefficient and slow since we never saw the full sequence context in a single pass. With this update, calibration is done once during the prefill stage using the full tokens chunk. This gives us much better coverage in a single run and completely removes the need for iterative decoding during calibration. After quantization, we take the KV cache encodings from the output, override the input KV cache encodings, and then propagate those into the graph that will later be lowered. This keeps everything consistent without needing to recalibrate on that graph. Result: same accuracy, significantly faster calibration, and a much cleaner separation between quantization and lowering ### Test plan Test CI in `TestExampleLLMScript` and `TestExampleMultimodalityScript`
1 parent 02e57bd commit f3d2077

6 files changed

Lines changed: 466 additions & 176 deletions

File tree

‎backends/qualcomm/tests/test_qnn_delegate.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7608,6 +7608,11 @@ def test_static_llm_model(self): # noqa: C901
76087608
case "sqnr":
76097609
cmds.extend(
76107610
[
7611+
"--skip_user_prompt_calibration",
7612+
"--tasks",
7613+
"wikitext",
7614+
"--limit",
7615+
"1",
76117616
"--eval_methods",
76127617
"sqnr_eval",
76137618
]

‎examples/qualcomm/oss_scripts/llama/decoder_runtime_evaluator.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ def __init__(
424424
self.max_seq_length = pte_max_context_len
425425

426426
def run(self, prompt):
427-
golden_logits, _ = INFERENCE_REGISTRY[True](
427+
result = INFERENCE_REGISTRY[True](
428428
get_example_inputs=self.get_example_inputs,
429429
prompt=prompt,
430430
module=self.source_model,
@@ -433,6 +433,7 @@ def run(self, prompt):
433433
use_i64_token=self.args.embedding_quantize is not None,
434434
collect_logits=True,
435435
)
436+
golden_logits = result.logits
436437

437438
input_file_name = f"{self.args.artifact}/input_tokens.raw"
438439

‎examples/qualcomm/oss_scripts/llama/decoder_utils.py‎

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ class DecoderInputs:
7777
embedding: Optional[torch.Tensor] = None
7878

7979

80+
@dataclass
81+
class DecoderOutputs:
82+
logits: Optional[torch.Tensor] = None
83+
token_list: Optional[List[int]] = None
84+
input_samples: Optional[List] = None
85+
86+
8087
class GraphModuleCalibrationWrapper(EagerEvalWrapper):
8188
"""
8289
A wrapper class for calibration
@@ -94,6 +101,7 @@ def __init__( # noqa: C901
94101
get_example_inputs: Callable,
95102
use_i64_token: bool,
96103
seq_mse_candidates: int,
104+
collect_input_samples: bool = False,
97105
):
98106
# n seq len = n-1 cache len, so we len(inps) = n-1 during _model_call
99107
assert max_seq_length is not None, "max_seq_length must be provided"
@@ -108,30 +116,32 @@ def __init__( # noqa: C901
108116
self.use_i64_token = use_i64_token
109117
self.seq_mse_candidates = seq_mse_candidates
110118
self._input_samples = None
119+
self.collect_input_samples = collect_input_samples
111120

112121
def get_input_samples(self):
113122
return self._input_samples
114123

115124
def _model_call(self, inps):
116-
all_logits = None
117125
kwargs = {}
118126
if self._use_kv_cache:
119127
kwargs["ar_len"] = self.ar_len
120128
kwargs["seq_mse_candidates"] = self.seq_mse_candidates
121129

122-
all_logits, self._input_samples = INFERENCE_REGISTRY[self._use_kv_cache](
130+
result = INFERENCE_REGISTRY[self._use_kv_cache](
123131
self.get_example_inputs,
124132
inps,
125133
self._model,
126134
self._tokenizer,
127135
max_seq_len=self.max_seq_length,
128136
use_i64_token=self.use_i64_token,
129137
collect_logits=True,
138+
collect_input_samples=self.collect_input_samples,
130139
**kwargs,
131140
)
141+
self._input_samples = result.input_samples
132142
# one shot is enough for seq mse
133143
self.seq_mse_candidates = 0
134-
return all_logits
144+
return result.logits
135145

136146

137147
class LookaheadDecoder:
@@ -727,7 +737,8 @@ def kv_inference( # noqa: C901
727737
collect_logits=False,
728738
seq_mse_candidates=0,
729739
lookahead_config=None,
730-
):
740+
collect_input_samples=False,
741+
) -> DecoderOutputs:
731742
input_samples = [] # Record input sample for quantization error analysis
732743
is_multimodal = all(
733744
[
@@ -814,6 +825,7 @@ def kv_inference( # noqa: C901
814825

815826
# record total input tokens and generated tokens
816827
total_token_list = prompt_token_list
828+
last_token_in_prompt = prompt_token_list[-1] if len(prompt_token_list) > 0 else None
817829

818830
# 3. prepare decoder inputs
819831
inputs = DecoderInputs(
@@ -841,28 +853,33 @@ def kv_inference( # noqa: C901
841853

842854
# Phase 2: Generate tokens until the EOS token is generated or max_seq_len is reached.
843855
# When run on wikitext for ppl evaluation, this while-loop is not expected to run.
844-
generate_input_sample = _generate(
845-
inputs,
846-
cur_pos,
847-
module,
848-
tokenizer,
849-
tok_embedding,
850-
ar_len,
851-
max_seq_len,
852-
k_caches,
853-
v_caches,
854-
total_token_list,
855-
lookahead_config,
856-
)
857-
if generate_input_sample is not None:
858-
input_samples.append(generate_input_sample)
859-
else:
860-
input_samples.append(prefill_input_sample)
856+
generate_input_sample = None
857+
if last_token_in_prompt != tokenizer.eos_id:
858+
generate_input_sample = _generate(
859+
inputs,
860+
cur_pos,
861+
module,
862+
tokenizer,
863+
tok_embedding,
864+
ar_len,
865+
max_seq_len,
866+
k_caches,
867+
v_caches,
868+
total_token_list,
869+
lookahead_config,
870+
)
871+
872+
if collect_input_samples:
873+
input_samples.append(generate_input_sample or prefill_input_sample)
861874

862875
logging.info(f"kv inference result:\n{tokenizer.decode(total_token_list)}")
863876
if collect_logits:
864877
result_logits = torch.cat(result_logits, dim=1)
865-
return result_logits, input_samples
878+
return DecoderOutputs(
879+
logits=result_logits if collect_logits else None,
880+
token_list=total_token_list,
881+
input_samples=input_samples if collect_input_samples else None,
882+
)
866883

867884

868885
@register_inference(use_kv_cache=False)
@@ -878,7 +895,8 @@ def prefill_inference(
878895
max_seq_len=512,
879896
use_i64_token=False,
880897
collect_logits=False,
881-
):
898+
collect_input_samples=False,
899+
) -> DecoderOutputs:
882900
input_samples = None # Record input sample for quantization error analysis
883901
is_multimodal = all(
884902
[
@@ -946,7 +964,11 @@ def prefill_inference(
946964
pos += 1
947965
if isinstance(prompt, str):
948966
logging.info(f"prefill inference result:\n{tokenizer.decode(token_list)}")
949-
return result_logits, [input_samples]
967+
return DecoderOutputs(
968+
logits=result_logits if collect_logits else None,
969+
token_list=token_list,
970+
input_samples=[input_samples] if collect_input_samples else None,
971+
)
950972

951973

952974
def graph_module_inference(
@@ -968,7 +990,8 @@ def graph_module_inference(
968990
event_name: Optional[str] = None,
969991
seq_mse_candidates: int = 0,
970992
lookahead_config: Optional[Tuple[int]] = None,
971-
):
993+
collect_input_samples: bool = False,
994+
) -> DecoderOutputs:
972995
"""
973996
This function supports model execution from static nn.Module decoder model
974997
all the way to edge program.
@@ -984,7 +1007,7 @@ def graph_module_inference(
9841007
kwargs["ar_len"] = ar_len
9851008
kwargs["lookahead_config"] = lookahead_config
9861009

987-
_, input_samples = INFERENCE_REGISTRY[use_kv_cache](
1010+
result = INFERENCE_REGISTRY[use_kv_cache](
9881011
get_example_inputs,
9891012
prompt,
9901013
module,
@@ -996,10 +1019,11 @@ def graph_module_inference(
9961019
max_seq_len=max_seq_len,
9971020
use_i64_token=use_i64_token,
9981021
collect_logits=False,
1022+
collect_input_samples=collect_input_samples,
9991023
**kwargs,
10001024
)
10011025
logging.info(f"Prompt summary for {event_name}")
1002-
return input_samples
1026+
return result
10031027
else:
10041028
calibration_wrapper = GraphModuleCalibrationWrapper(
10051029
model=module,
@@ -1010,6 +1034,7 @@ def graph_module_inference(
10101034
get_example_inputs=get_example_inputs,
10111035
use_i64_token=use_i64_token,
10121036
seq_mse_candidates=seq_mse_candidates,
1037+
collect_input_samples=collect_input_samples,
10131038
)
10141039
with torch.no_grad():
10151040
eval_results = simple_evaluate(
@@ -1022,4 +1047,4 @@ def graph_module_inference(
10221047
for task, res in eval_results["results"].items():
10231048
logging.info(f"{task}: {res}")
10241049

1025-
return calibration_wrapper.get_input_samples()
1050+
return DecoderOutputs(input_samples=calibration_wrapper.get_input_samples())

‎examples/qualcomm/oss_scripts/llama/llama.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,12 @@ def _build_parser():
577577
help="Enable automatic quant recipe suggestion in PTQ",
578578
)
579579

580+
parser.add_argument(
581+
"--skip_user_prompt_calibration",
582+
action="store_true",
583+
help="Skip using user prompt for calibration. Useful when only dataset-based calibration is desired.",
584+
)
585+
580586
return parser
581587

582588

@@ -676,6 +682,9 @@ def export_llama(args) -> None:
676682
assert (
677683
not is_multimodal or args.use_attention_sink is None
678684
), "Multimodal models currently do not support attention sink feature."
685+
assert (
686+
not is_multimodal or not args.skip_user_prompt_calibration
687+
), "--skip_user_prompt_calibration is not supported for multimodal models (VLM/ALM) as they do not support task-based calibration yet."
679688

680689
if args.pre_gen_pte:
681690
text_decoder_pte_path = f"{args.pre_gen_pte}/{pte_filenames[TEXT_DECODER]}.pte"

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
class Mode(Enum):
4141
PREFILL = 1
4242
DECODE = 2
43+
CALIBRATE = 3
4344

4445

4546
def log_info(func):
@@ -83,7 +84,7 @@ def process_model_args(
8384
model_args: ModelArgs object to be modified.
8485
quant_recipe: Quantization recipe to be used.
8586
config: LLMModelConfig object to be used.
86-
mode: Mode of operation (PREFILL or DECODE).
87+
mode: Mode of operation (PREFILL, DECODE, or CALIBRATE).
8788
"""
8889
# TODO: support batch inputs if necessary
8990
if mode == Mode.DECODE:
@@ -95,13 +96,19 @@ def process_model_args(
9596
if control_args.model_mode == "lookahead"
9697
else 1
9798
)
98-
else:
99+
elif mode == Mode.PREFILL:
99100
ar_len = control_args.prefill_ar_len
101+
elif mode == Mode.CALIBRATE:
102+
ar_len = control_args.max_context_len
103+
else:
104+
raise ValueError(f"Unsupported mode: {mode}")
100105

101106
model_args.max_batch_size = 1
102107
model_args.max_seq_len = control_args.max_seq_len
103108
model_args.max_context_len = control_args.max_context_len
104-
model_args.use_kv_cache = control_args.max_context_len != ar_len
109+
model_args.use_kv_cache = (
110+
control_args.max_context_len != ar_len or mode == Mode.CALIBRATE
111+
)
105112
model_args.enable_r3 = config.r3
106113
model_args.ar_len = ar_len
107114
model_args.kv_io_bit_width = quant_recipe.get_kv_io_bit_width()

0 commit comments

Comments
 (0)