Skip to content

Qualcomm AI Engine Direct - [GenAI Pipeline] Multimodal LLM & LLM Preparation and Quantization - #23050

Open
DannyYuyang-quic wants to merge 1 commit into
pytorch:mainfrom
CodeLinaro:dev1/danny/pr-A1
Open

DannyYuyang-quic wants to merge 1 commit into
pytorch:mainfrom
CodeLinaro:dev1/danny/pr-A1

Conversation

@DannyYuyang-quic

@DannyYuyang-quic DannyYuyang-quic commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

First PR of Phase 2 Stream A. This PR makes the GenAI Pipeline runnable for
LLM and MLLM models through the front half of the flow:

CLI -> model lookup -> source transforms -> model preparation
    -> dataset-backed calibration -> quantization -> save QDQ model

examples/ and the legacy llama.py path remain unchanged. Compilation and
device inference remain in Stream B / PR-A2.

What's Included

CLI Entry Point: cli.py

Adds a GenAI Pipeline CLI that resolves model, dataset, and quantization options, builds PipelineContext, and invokes model preparation and quantization. FP16 omits quantization; QAT, embedding quantization, and attention sink are rejected for now.
The rejected feature will be raised in following PR.

Registry-Backed Model lookup: model_lookup.py

Maps a user-facing model name to the model-specific inputs consumed by the pipeline. Callers resolve one registry config, then derive from that same entry:

  • Model construction: component and graph keyed constructors, including
    decoder decode/quantize graphs, optional prefill graphs, and multimodal
    embedding variants.
  • Source preparation: component-keyed state-dict weight transforms and
    module G2G transforms, plus the Hugging Face state-dict loader when needed.
  • Pipeline configuration: component sharding, quantization dtypes and
    recipe classes, and LLM or MLLM loader/quantizer adapters.

This centralizes model-family branching at the registry boundary; the model preparation and quantization stages consume only the derived component and graph keyed configuration.

LLM and Multimodal LLM model components: model_components/

Adds pipeline import surfaces for decoder, token embedding, and encoder components over existing examples implementations.

Module G2G Transforms: source_transform/:

Collects existing llama.py transformations into reusable pipeline functions:

  • Weight transforms.
  • Model graph-to-graph (G2G) transforms.

LLM and MLLM Model Loading and Preparation: strategies/model_preparation/:

ExecuTorchModelPreparationStrategy implements the shared preparation flow:

  1. Load one weighted module per component and graph variant.
  2. Read per-graph metadata.
  3. Build model-native example inputs for each graph.
  4. Select one weight-sharing module per component.
  5. Apply component-level module transforms.
  6. Load the tokenizer.
  7. Build the model-specific inference helper used by quantization, if any.
  8. Optionally export the tokenizer for runtime.
  9. Extract a tokenizer chat template, falling back to extra_options.

LLMLoaderAdapter handles text decoders; MLLMLoaderAdapter handles multimodal models. The default adapter owns shared tokenizer and transforms logic.

Dataset stack: datasets/

Adds typed dataset options, lookup, loaders, calibration adapters, and LLM/MLLM collators. It supports random fallback data, lm-eval samples, JSON messages, Hugging Face chat datasets, and multimodal messages.

generate_calibration_data() now receives example_inputs explicitly because collators require the calibration-graph signature.

Quantization: strategies/quantization/:

ExecuTorchQuantizationStrategy implements native PTQ for component and graph keyed models:

  1. Create one QNN quantizer per component graph.
  2. Export every graph variant and prepare it with observers.
  3. Initialize observers for deploy graphs.
  4. Build real calibration inputs through the calibration data adapter.
  5. Run PTQ calibration on graphs that consume real data.
  6. Convert prepared graphs to QDQ modules.
  7. Propagate encodings from quantize graphs to deploy graphs.
  8. Remove quantize-only graphs from the output.

Decoder and token embedding use separate quantize and deploy graphs; encoders use one shared graph. Encodings are propagated before lowering.

Quantizer creation belongs to the strategy; LLM/MLLM adapters own only model-family-specific export, preparation, conversion, and calibration.

Quantization Helpers: quant_utilities.py:

Provides QNN quantizer construction, recipe application, QDQ saving, logits/KV-cache attributes, and encoding propagation. KV-cache override is enabled only when n_cache_layers is provided.

Quantization Output

QuantizationOutputConfig now returns component and graph keyed GraphBundle objects. Each bundle carries the quantized graph module, export inputs, metadata, and optional quantized IO dtypes.
Quantize-only graphs are removed after their encodings have been propagated, so downstream compilation sees only deployment graphs.

Tests

Covers CLI/config construction, source transforms, datasets and collators, loader adapters, model preparation, quantization adapters and strategy, and preparation/quantization stage integration.

PR Review Checklist

  • All dependencies are injected via constructor with sensible defaults - Yes.
  • All external calls are behind injectable interfaces - Yes
  • Hugging Face, TokenizerWrapper, torch.export, PT2E, QNN quantizer construction, dataset
    loading, and calibration execution sit behind adapters or lookup-created
    callables. - Yes.
  • Unit tests cover the new public behavior - Yes.
  • Docstrings on public classes and methods - Yes.
  • Type annotations on function signatures - Yes.
  • Logging follows the strategy in the LLD - Yes; info for stage-level progress, debug for per-step details, warning for degraded/normalized CLI behavior. No existing behavior changed - Yes
  • examples/ is not modified and llama.py remains the reference path. - Yes

Related PRs

Phase 2 flow. PR-A1 and PR-B1 are independent and can land in either order.
PR-B2 depends on PR-B1. PR-A2 depends on PR-A1 and PR-B2, and closes Phase 2 by wiring the runner and adding the parity test that gates Phase 3 deletion of the legacy flow.

PR-A1 (this PR) ────────────────┐
                                ├─► PR-A2
PR-B1 ───────────► PR-B2 ───────┘

Phase 1 (merged):

Phase 2:

  • PR A1: Model registry, model_lookup, CLI, Multimodal LLM & LLM model preparation,
    dataset-backed calibration, component/graph-aware quantization, and encoding
    reconciliation. [This PR].
  • PR B1: Compilation foundation: Qualcomm AI Engine Direct - [GenAI Pipeline Phase 2] PRB1 - Compilation foundation #22846
  • PR B2: Multi-graph lowering, weight sharing, sharding, and spill-fill.
    Depends on PR-B1.
  • PR A2: Device-runner adapter, pipeline-runner wiring, README, and E2E parity
    test. Depends on PR-A1 and PR-B2.

Test plan

Run only tests added in this PR:

python -m pytest \
  backends/qualcomm/genai_pipeline/tests/strategies/model_preparation/ \
  backends/qualcomm/genai_pipeline/tests/strategies/quantization/ \
  backends/qualcomm/genai_pipeline/tests/source_transform/ \
  -v

Result:

126 passed, 2 subtests passed in 11.59s

Run all genai_pipeline tests:

python -m pytest backends/qualcomm/genai_pipeline/tests/ -v

Result:

238 passed, 13 subtests passed in 14.37s

Run all tests with coverage:

python -m pytest backends/qualcomm/genai_pipeline/tests/ \
  --cov=backends/qualcomm/genai_pipeline \
  --cov-config=backends/qualcomm/.coveragerc \
  --cov-report=term-missing

Result:

238 passed, 13 subtests passed in 17.41s

Confirm the legacy flow is unaffected:

python -c "
from executorch.examples.qualcomm.oss_scripts.llama.llama import _build_parser, export_llama
from executorch.examples.qualcomm.oss_scripts.llama.wrappers import MultiModalManager, HybridAttentionSinkEvictor
print('LEGACY IMPORTS OK')
"

Result:

`LEGACY IMPORTS OK`

@pytorch-bot

pytorch-bot Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/23050

Note: Links to docs will display an error until the docs builds have been completed.

❌ 1 New Failure

As of commit 7fdb147 with merge base 91d26b3 (image):

NEW FAILURE - The following job has failed:

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Sep 23, 2026
@DannyYuyang-quic

Copy link
Copy Markdown
Contributor Author

@pytorchbot label "release notes: qualcomm"

@pytorch-bot pytorch-bot Bot added the release notes: qualcomm Changes to the Qualcomm backend delegate label Sep 23, 2026
@DannyYuyang-quic

DannyYuyang-quic commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor Author

Hi @psiddh,

TL;DR:

This PR makes the GenAI Pipeline runnable through backends/qualcomm/genai_pipeline/cli.py for the models supported by the existing llama.py flow. It covers model preparation and basic dataset-backed PTQ calibration, producing quantized QDQ models.

The existing examples/ and legacy llama.py flow remain unchanged.
Advanced features, including QAT and AttentionSink, are intentionally out of scope for GenAI Pipeline and
will be introduced in a follow-up PR.

Together with PR B1, Compilation Foundation (#22846), the following commands
produce quantized QDQ models:

LLM

python -m backends.qualcomm.genai_pipeline.cli --model gemma3-1b --soc SM8750 --calib-samples examples/qualcomm/oss_scripts/llama/assets/samples/text.json --compile-only --max-seq-len 1024

Multimodal LLM

Vision-Language model

python -m backends.qualcomm.genai_pipeline.cli --model internvl3_1b --soc SM8750 --calib-samples examples/qualcomm/oss_scripts/llama/assets/samples/vision.json --compile-only --max-seq-len 1024

Audio-Language model

python -m backends.qualcomm.genai_pipeline.cli  --model granite_speech_3_3-2b --soc SM8750 --calib-samples examples/qualcomm/oss_scripts/llama/assets/samples/audio --compile-only --max-seq-len 1024

Please have a look. Thanks!

cc: @shewu-quic @winskuo-quic @qti-horodnic

…Quantization

Add the LLM/MLLM GenAI pipeline integration for model preparation, dataset-driven calibration, and ExecuTorch quantization.

Summary:
  - Add the GenAI pipeline CLI and stage context wiring.
  - Add model registry lookup helpers for configs, graph builders, source
    transforms, checkpoint loaders, quantization settings, and adapters.
  - Add component-aware LLM/MLLM model preparation adapters for decoder,
    embedding, vision, and audio modules.
  - Add dataset adapters, collators, and dataset option for
    calibration, training, and evaluation.
  - Add ExecuTorch quantization support for export/prepare, encoding
    initialization, calibration, encoding override, conversion, and QDQ EP save.
  - Add source transforms for checkpoint remapping, dtype override, embedding
    scaling, RoPE layout, RMSNorm offset, and linear-to-conv2d conversion.
  - Expand unit coverage for model preparation, quantization, source transforms,
    datasets, and pipeline stage behavior.
return LLMCalibrationDataAdapter(**kwargs)


def get_training_dataset_adapter(dataset_options, is_multimodal=False):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_training_dataset_adapter / get_eval_dataset_adapter

Both call get_dataset_adapter(...), which only reads the calib_* options — so --train-tasks, --train-hf-dataset, --train-limit, --eval-tasks, --eval-limit and --eval-num-fewshot never reach a loader, and training/eval get the calibration corpus instead. Is purpose-specific selection planned for a later PR, or should get_dataset_adapter take the purpose now?

config = process_model_args(
control_args,
ModelArgs(**base_args),
quant_recipe(mode == Mode.CALIBRATE),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

quant_recipe can be None — get_quant_recipe returns getattr(config, "quant_recipe", None) — and the gemma4 branch guards for exactly that (quant_recipe().get_kv_io_bit_width() if quant_recipe else 32). This call doesn't, so any registry row without a recipe raises TypeError: 'NoneType' object is not callable.

"attention sink is not yet supported in GenAI Pipeline.",
args.max_seq_len,
)
args.max_context_len = args.max_seq_len

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--max-context-len is accepted, then unconditionally overwritten. Since attention sink isn't supported yet, I'd reject it in _validate_args alongside --qat and --use-attention-sink rather than silently ignoring a value the user set.

Same thought for the rest of the parser: with compilation_stage=None and inference_stage=None, --prompt, --temperature, --audio-path, --image-path, --pre-gen-pte and --eval-methods are all no-ops, and --compile-only just logs a line. Worth gating them the same way so the CLI doesn't advertise more than it does.

_GRAPH_ROLES = {
# The quantize graph supplies encodings for the decoder's deployed variants.
ARTIFACT_TEXT_DECODER: {
GRAPH_FORWARD: _GraphRole.QUANTIZE,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct today (GRAPH_FORWARD = "forward" isn't in DECODER_GRAPH_NAMES), but it's order-dependent: if GRAPH_FORWARD ever joins that list the ** overwrites QUANTIZE with DEPLOY, the quantize graph stops receiving calibration data, and nothing errors — you just get uncalibrated encodings. One assert GRAPH_FORWARD not in DECODER_GRAPH_NAMES would pin it.

input_config: Graph inputs and metadata used for observer execution.
"""
for component, prepared_graphs in prepared_modules.items():
for graph_name, graph_module in prepared_graphs.items():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No break, so the last non-deploy graph wins — while _flatten_calibration_example_inputs does break and takes the first. Benign while each component has exactly one QUANTIZE graph, but the module and its example inputs could silently come from different graphs if that ever changes. Worth making the two loops agree.

attr = getattr(attr, target_atom)
return attr

def _set_attr(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These duplicate the get_attr_from_target / set_attr_from_target helpers that the HF LLM Optimization PR adds to backends/qualcomm/builders/utils.py (and removes from llm_wrappers.py for the same reason). Worth coordinating so one copy survives.

@psiddh

psiddh commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

do you know what these failure are ? MLTEC_elastic - GitHubPOC - executorch dev1-danny-pr-A1 — The build failed

@psiddh

psiddh commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

@claude can you thoroughly review this large PR from arch and direction point of view . Also review my comments and overall sugegst fi there are any blockers / concerns

@claude

claude Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Claude finished @psiddh's task in 1m 26s —— View job


Summary: Request changes. The pipeline structure works, but I found three correctness blockers: multimodal quantization fails in GraphBundle validation, runs without a calibration source fail late, and mixed-case model names silently take the wrong code path. The bigger direction problem is that the new library depends on examples/ and copies legacy logic without a parity check, which conflicts with the Phase 3 plan to delete the legacy flow.

PR Review: #23050 (architecture / direction, plus your comments)

Summary

This PR adds the front half of the Qualcomm GenAI Pipeline: CLI, registry lookup, source transforms, LLM/MLLM model preparation, dataset-backed calibration, and component/graph-keyed PTQ that produces QDQ graphs. All 77 changed files are under backends/qualcomm/genai_pipeline/.

Everything below comes from reading the code. I could not run Python or the tests in this environment.


🚫 Blockers

1. Multimodal quantization always fails at the last step.

  • EncoderQuantRecipe only defines get_act_bit_width(), which returns 16 (examples/qualcomm/oss_scripts/llama/encoder/encoder_quant_recipe.py:33).
  • So _get_quant_io_dtypes (executorch_quantization_strategy.py:728-757) returns {"io_type": torch.uint16} for the vision and audio encoder graphs, with no kv_type.
  • GraphBundle.__post_init__ (graph_bundle.py:116-131) rejects any mapping that doesn't have exactly {kv_type, io_type}. The ValueError is caught and re-raised as a generic StageError("Quantization failed"), after all export and calibration work is done.
  • GraphBundle's own docstring says encoders should get None.
  • No test covers an encoder recipe here.
  • @DannyYuyang-quic, the internvl3_1b / granite_speech commands in your comment look like they can't finish on this commit. Were they run against a different local state?
  • Fix this →

2. The "random fallback" calibration path is broken.

  • With no --calib-* source, get_calibration_dataset_adapter (dataset_lookup.py:95-107) returns DefaultCalibrationDataAdapter, which yields a plain list of tuples.
  • LLMQuantizerAdapter.calibrate (llm_quantizer_adapter.py, around line 111) raises ValueError("Calibration requires a corpus-backed DataLoader…").
  • MLLMQuantizerAdapter.calibrate (around line 130) indexes text_batch["input_ids"] on a tuple, which raises TypeError.
  • Both happen after export, prepare and observer init.
  • The PR description lists "random fallback data" as supported, so either make it work or reject the run up front, as legacy build_calib_dataloaders does ("No calibration data specified…").
  • Separately, the random path ignores --max-seq-len: line 220 passes no extra_options, so it always uses 1024.

3. Mixed-case model names silently take the wrong code path.

  • get_model_config (model_lookup.py:76), get_source_transform (:351) and get_state_dict_loader (:482) lowercase the name.
  • get_model_arch_config (:113), get_model_arch (:198) and the LLM_VARIANT_ARCHS lookup (:283) compare the raw string.
  • Example: --model Gemma3-1B passes the registry lookup but builds LlamaModel instead of MultiScopeAwareLlamaModel.
  • Example: --model Gemma4-E2B falls into the ModelArgs(**json) path and crashes.
  • Fix: normalize once in the CLI, or add choices= to --model.

4. The quantization output no longer matches what compilation reads (merge-order risk).

  • genai_pipeline.py:281-289 passes quant_output.graphs, a {component: {graph: GraphBundle}} dict, as CompilationInputConfig.model. It also passes nested example_inputs as example_inputs.
  • The FP16 fallback (model_prep_output.model_module) is a component dict now too.
  • ExecuTorchCompilationStrategy / default_compiler_adapter.py:269-271 on main hand these straight to to_edge_transform_and_lower_to_qnn, which expects one module and one tuple.
  • It is hidden only because cli.py wires compilation_stage=None.
  • The PR says A1 and B1 can land in either order. In both orders main has an orchestrator whose stages disagree. Either reconcile the hand-off in whichever PR lands second and say so explicitly, or fail fast in _run_compilation when it receives the nested shape.

Architecture / direction concerns

A. The dependency direction is inverted, so Phase 3 can't delete the legacy flow as planned.

  • The library under backends/qualcomm/genai_pipeline imports these from examples/:
    • the model registry (SUPPORTED_LLM_MODELS, LLMModelConfig, LLM_VARIANT_ARCHS)
    • all quant recipes
    • static_llama / apply_rope / feed_forward / layernorm / encoders
    • wrappers/base_component (process_model_args, Mode, get_model_specific_kwargs)
    • gemma4 config and wrapper, hf_download, the dataset loaders, inference, tokenizer, and safe_dataloader_iter
  • models/, recipes/ and model_components/ are re-export shims. They hide the dependency rather than reverse it.
  • wrappers/base_component.py itself imports the full registry, so deleting "only the legacy flow" breaks model_lookup.py.
  • Before Phase 3, the shared pieces (registry, recipes, static model code, process_model_args) need to move into backends/qualcomm/, with llama.py importing from there.
  • Please write that plan into the LLD now. Otherwise A2's parity test will gate a deletion that can't actually happen.

B. Legacy logic is copied rather than shared, with no parity check, so the two copies will drift.

  • quant_utilities.encoding_override copies HybridTextDecoder._encoding_override (llm_wrappers.py:909-1061).
  • is_node_src_start_with_name, save_logits_quant_attrs and save_output_kv_cache_quant_attrs are also copies (llm_wrappers.py:125-157, 533-563).
  • All six source_transform/* files copy _prepare_model.
  • Until A2 there is no test that compares new output against legacy output: no state dict comparison, no QDQ encodings, nothing.
  • Suggest one of two things:
    • make llm_wrappers.py call the new functions in this PR, so there is a single copy; or
    • add a small parity test now (e.g. stories260k, comparing encodings / state dicts). That would also catch the divergences in E.

C. Model-family branching is spread across model_lookup.py as hard-coded name checks, not driven by the registry.

  • I count 6 string checks: gemma4-e2b at :113, :198, :366 and :483; ("gemma-2b","gemma2-2b","gemma3-1b") at :369; stories260k at :380.
  • Adding a model therefore means editing several functions, not just registering one row.
  • Params-path resolution, JSON loading and Gemma4 config setup are each repeated (:118-138 vs :207-269). The params file is parsed up to 3× per run, and process_model_args can re-run setup_qnn_sdk() just to read shapes.
  • These should be fields on LLMModelConfig (transforms, state-dict loader, arch builder).

D. The LLM/MLLM adapter split doesn't separate the logic it is meant to separate.

  • ExecuTorchQuantizationStrategy is supposed to be generic, but it hard-codes decoder/embedding knowledge:
    • _GRAPH_ROLES lists all five components.
    • _post_process_example_inputs hard-codes the decoder's positional tuple layout ([0], *[1], [2], *[3], *[4]) and the "get_use_kv_cache" meta key.
    • _override_encodings hard-codes the decoder→prefill and tok-embedding flows and the get_n_self_layers meta key.
  • Meanwhile LLMQuantizerAdapter and MLLMQuantizerAdapter are nearly identical apart from calibrate. MLLMLoaderAdapter likewise re-implements most of LLMLoaderAdapter.
  • The model-family logic should sit behind the adapter (or a per-component role hook). Otherwise every new component type means editing the strategy.

E. There are behavioral differences from legacy that A2's parity test will hit.

  • tok_embedding quantizer: it has no recipe, so it uses make_quantizer defaults (per_channel_linear=False, MovingAverageMinMaxObserver). Legacy uses use_16a8w, per_channel_linear=True, MinMaxObserver (llm_wrappers.py:718-725). Its IO dtype tag is also None, where legacy tags it with the decoder's kv/io types (:862-869).
  • Prefill encoding override: it always runs with n_cache_layers. Legacy gates it on prefill.meta["get_use_kv_cache"] (:1117-1120).
  • No recipe: a model without a recipe still gets quantized with 16a8w defaults, while legacy skips quantization (:672-673).
  • Gemma4: the calibration graph uses max_batch_size=batch_size and lookahead decode uses ar_len=next_power_of_two(...). Legacy uses 1 for both.
  • Multimodal calibration: it ignores --calib-tasks / --calib-hf-dataset (dataset_lookup.py:25-37). Legacy feeds them to the text decoder.
  • Encoder weights: they load with strict=False and missing keys aren't reported, and legacy's get_n_layers check was dropped. A renamed key leaves random weights with no error.

F. One module is used to export every graph, picked by dict order.

  • _get_component_module keeps next(iter(graph_modules)), which is the decode module because of insertion order in get_model_arch. That module is then exported with the calibration and prefill example inputs.
  • use_kv_cache is fixed when the attention layers are built (static_llama.py:281). So hybrid mode with --prefill-ar-len == --max-seq-len (and the CLI forces max_context_len = max_seq_len) builds a prefill graph with no KV inputs, exported through a KV module. That fails at trace time.
  • Selection should be explicit, or the design should be one weight module plus per-graph shape specs.
  • Every graph's module is also fully constructed in fp32 before the weights are shared, about 3× transient memory in hybrid mode. Legacy does the same, but meta-device construction would avoid it.

G. Phase 1 interfaces changed in breaking ways, and the default adapters stop working.

  • QuantizerAdapter dropped make_quantizer, added init_encodings, and changed calibrate's signature. DefaultQuantizerAdapter still uses the old parameter names.
  • ModelLoaderAdapter gained 5 methods.
  • QuantizationInputConfig lost calibration_data / training_data, but its docstring still documents training_data, and quant_recipe is kept but never read.
  • DefaultModelLoaderAdapter returns only {TEXT_DECODER: {GRAPH_FORWARD: m}}, which the strategy treats as quantize-only. A default-constructed strategy therefore produces zero deployable graphs.
  • This is acceptable before a release, but the defaults should either stay usable or be removed.

H. Hidden state is passed around, against the CLAUDE.md rule on explicit state.

  • Loading is driven by an untyped extra_options["model_options"] dict (partials, transforms, loaders) that every stage receives.
  • The loaders mutate the shared CLI namespace (llm_loader_adapter.py:291-296, mllm_loader_adapter.py:413-418).
  • Methods are probed with getattr/hasattr throughout (get_example_inputs / get_example_input, get_metadata, chat_template, prepare_attention_conv, encoder configs).
  • One concrete result: cli.py:537 nests embedding_quantize under model_options, but the loaders read it from the root (llm_loader_adapter.py:262). This is latent only because the CLI currently rejects the flag.
  • Typed fields on the configs would fix this.

Code Quality

  • model_components/decoder/__init__.py:59-62: AttentionMask, BaseAttentionMask, CausalAttentionMask and SlidingWindowAttentionMask are in __all__ but never imported, so import * raises AttributeError.
  • model_lookup.py:36-42 imports model_components.decoder and models eagerly, which defeats the lazy-import reasoning in model_components/__init__.py.
  • recipes/__init__.py is unused. It also omits Gemma4QuantRecipe even though gemma4 is registered.
  • export_tokenizer in both loaders ignores output_dir, which contradicts the protocol docstring.
  • Stale docs:
    • genai_pipeline.py:100-103 describes a {graph_name: Mode} plan the orchestrator doesn't make.
    • chat_template is typed Optional[str] but receives a bound method from TokenizerWrapper.
  • Ineffective cleanup: pop() plus gc.collect() at executorch_quantization_strategy.py:589-592 / 615-618 frees nothing while the locals still hold the graph. prepared_modules.clear() at :486 frees nothing either, because convert_pt2e works in place.
  • cli.py:745: the blanket except KeyError logs only "Error: %s" and swallows unrelated KeyErrors with no traceback.

Testing

  • The tests mostly use mocks: torch.export, prepare_pt2e and torch.load are patched. No test runs even a tiny real model (e.g. stories260k, or a small LlamaModel(ModelArgs)) through lookup → load → transform → prepare → calibrate → convert. That kind of test would have caught Blockers 1, 2 and 4 and item F.
  • There are no tests for cli.py or dataset_lookup.py.
  • test_executorch_model_preparation_strategy.py:339 puts embedding_quantize at the root of extra_options, so it hides the nesting bug described in H.

Your review comments: verdicts

# Comment Verdict
1 dataset_lookup.py:218 train/eval adapters only read calib_* Valid. No effect today: QAT is rejected, and the strategy only calls generate_calibration_data. But --train-* / --eval-* are accepted and silently ignored. DatasetOptions.train_val_ratio is never read, and the default train/eval adapters ignore num_samples. Either take a purpose now, or drop these functions and flags until QAT/eval lands.
2 model_lookup.py:276 quant_recipe can be None Inconsistent, but can't happen today. All 20 registry rows set quant_recipe, and LLMModelConfig has no None default. A guard here wouldn't help anyway, because process_model_args calls quant_recipe.get_kv_io_bit_width() unconditionally (base_component.py:117). The cleaner fix is the reverse: remove the dead if quant_recipe else 32 guard and its stale "32 sentinel" comment, since the base recipe __init__ raises when default_quant_dtype is None.
3 --max-context-len overwritten; no-op flags Valid, and the list is longer. --system-prompt and --quant-recipe-suggestion also have no consumer. --use-fp16 runs only model prep and produces nothing. The module docstring advertises prompt-based generation. Suggest defaulting --max-context-len to None and rejecting it when it differs from --max-seq-len, and rejecting or hiding the inference/compile flags until A2.
4 _GRAPH_ROLES relies on dict ordering Valid, future risk only. "forward" is in neither DECODER_GRAPH_NAMES nor TOK_EMBEDDING_GRAPH_NAMES today. The same pattern is used for tok_embedding. A module-level assert is enough.
5 _calibrate has no break, so it disagrees with _flatten_calibration_example_inputs Valid, future risk only (lines :460-464 vs :672-677). Every component has exactly one non-deploy graph today. Use one shared selection helper.
6 _get_attr / _set_attr duplicates Valid, and the fix is available now. backends/qualcomm/builders/utils.py:20-37 in this tree already has get_attr_from_target / set_attr_from_target with identical bodies, and legacy already imports them (llm_wrappers.py:25-27). Import them instead of waiting on the other PR. This is one case of the wider duplication in B.

NXP Bamboo failure: that CI is NXP-hosted, and I can't see its logs. This PR changes only files under backends/qualcomm/genai_pipeline/, with nothing in examples/, backends/nxp, the build files or CMake, so the failure is very likely unrelated or infra. Worth checking with the NXP folks, or comparing against the in-repo "Test NXP Backend" workflow once it's approved.


Recommendation

Request Changes

  • Must fix before merge: Blockers 1–3.
  • Must document or fix before merge: Blocker 4, the stage hand-off and merge order.
  • Direction issues to settle now, ideally in the LLD, since they decide whether Phase 3 is feasible:
    • A: move the shared registry, recipes and model code out of examples/.
    • B: keep one copy of the duplicated legacy logic, or add an early parity test.
    • D: move the model-family logic out of the "generic" strategy.
  • Can follow up: C and E–H, as long as they are tracked, because A2's parity test will run into the items in E.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. release notes: qualcomm Changes to the Qualcomm backend delegate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants