Skip to content

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

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

DannyYuyang-quic wants to merge 3 commits 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.

✅ No Failures

As of commit 7987450 with merge base 91d26b3 (image):
💚 Looks good so far! There are no failures yet. 💚

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The training and evaluation dataset support is planned for a follow-up PR.
I also agree that we should remove the unsupported CLI flags for now and add them back when the corresponding functionality is implemented.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for pointing this out. I'll make the handling consistent by checking whether quant_recipe is None before using it.

Comment thread backends/qualcomm/genai_pipeline/cli.py Outdated
"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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I agree that we should remove the unsupported flags for now. So. we don't need to add validation for flags that we're going to remove anyway. We can add them back once the corresponding functionality is supported.

_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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the suggestion. I think assert GRAPH_FORWARD not in DECODER_GRAPH_NAMES is a simple and effective way to make this assumption explicit and prevent it from being accidentally broken in the future. I'll use this approach.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for catching this. This is indeed a potential issue. I'll make the two loops consistent by breaking on the first matching graph.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, we should reuse the helpers from backends/qualcomm/builders/utils.py here. I missed this integration. Thanks for catching it.

@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.

@DannyYuyang-quic

Copy link
Copy Markdown
Contributor Author

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

I'm not sure what this failure is. This is the first time I've seen it, and I'm also unable to access the link.
I can try pushing another change later to rerun the CI and see if it happens again.

@psiddh

psiddh commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Can you also check the linter + 3 blocker issues that Claude review flagged above. Thanks

@DannyYuyang-quic

Copy link
Copy Markdown
Contributor Author

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)

1. Multimodal quantization / _get_quant_io_dtypes

Fixed the _get_quant_io_dtypes and GraphBundle issue and re-tested the flow successfully.

2. Random fallback calibration

Updated the behavior here. Random fallback is now blocked for multimodal models and is only supported for Llama as an example/fallback path.

3. Mixed-case model names

Fixed. The model name is now normalized to lowercase before model lookup.

4. Compilation hand-off

The compilation stage integration will be covered in the follow-up PR A2.

A. Dependency on examples/

Agreed. We plan to clean up and reorganize the shared components currently under examples/ in follow-up PRs.

B. Duplicated legacy logic / parity

B1. Source transforms

The source transforms are intentionally separated because each model family has its own transformation requirements. The goal is to decouple the model-specific logic rather than keep it in a shared legacy path.

B3. Parity / evaluation

A follow-up PR will validate the final quantization results through evaluation accuracy

C. Registry-driven model-family logic

Each source transform requires model-specific inputs and handling, so we don't think driving all transformation logic directly through the registry is a good fit here.

D. Strategy / Adapter responsibilities

D1. Strategy responsibility

The current responsibility split is intentional: the strategy owns the routing/orchestration logic, while adapters provide the concrete model-specific implementations. Because of that, the strategy is expected to have some knowledge of decoder/embedding graph roles.

D2. LLM / MLLM adapters

The LLM and MLLM quantization adapters are intentionally kept separate because they support different sets of quantization algorithms. LLM currently supports more quantization algorithms than MLLM, so keeping separate adapters provides the flexibility to evolve their quantization capabilities independently.

E. Behavioral differences from legacy

E1. tok_embedding quantizer

Fixed. The default quantizer behavior has been updated to match the legacy behavior.

E2. Prefill encoding override

Fixed. The prefill encoding override behavior is now aligned with the legacy path.

E3. No recipe

Updated. When no recipe is provided, we now fall back to a dtype-driven quantizer. Whether quantization is enabled is consistently controlled by the FP16 flag.

E5. Multimodal calibration

Updated. Multimodal calibration now explicitly rejects --calib-tasks and --calib-hf-dataset instead of silently ignoring them.

E6. Encoder weights

strict=False for encoder weight loading is existing legacy behavior, so this PR intentionally preserves the same behavior.

F. Module selection across graph shapes

The module weights are identical across the different input-shape graphs, so selecting one module does not affect the weights or result.

That said, I've added a TODO to decouple the module architecture from the example inputs, which would remove the need to select one module from multiple graph entries.

G. Phase 1 interfaces / default adapters

I removed the outdated training_data documentation from QuantizationInputConfig.

Regarding DefaultModelLoaderAdapter, the current behavior of returning {TEXT_DECODER: {GRAPH_FORWARD: m}} is expected. The loader is responsible for providing the underlying model module, and for the text decoder there is only one nn.Module. The decode/prefill/etc. graphs are different graph representations derived from that same module, rather than separate model modules. Therefore, having a single module in the default loader output is intentional and does not imply that only one deployable graph should be produced.

H. Hidden state / extra_options

H1. extra_options

extra_options intentionally remains a dictionary to support extensible model-specific options without requiring the shared config dataclasses to change for every new option.

H2. Shared CLI namespace

The loader-side namespace mutation is currently a workaround for the TokenizerWrapper interface, which expects the original argument names. We plan to clean this up in a follow-up.

H3. getattr / hasattr

The getattr/hasattr checks are currently needed for compatibility with the legacy path. We plan to clean up and formalize these interfaces in a follow-up PR.

H4. embedding_quantize

Fixed. embedding_quantize is now read from the correct level of extra_options.

@psiddh

psiddh commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

@claude can you review the latest changes and make sure all the comments / feedback was addressed ?

@claude

claude Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Claude finished @psiddh's task in 5m 7s —— View job


Summary: Most of the feedback is addressed in 55cc8f6, but two of the three original blockers are only partly fixed: an LLM run with no --calib-* source still fails late, and mixed-case --model names still reach TokenizerWrapper unnormalized. The fix commit also adds one small regression: --soc lost its default but isn't marked required.

Re-review of #23050: status of every earlier finding and inline comment

I reviewed 55cc8f6 ("Fix GenAI pipeline quantization and calibration behavior") against the earlier review and psiddh's inline comments. I could not run the tests or lint: flake8, ufmt and lintrunner need approval in this sandbox. So the linter status is unverified, and @psiddh's lint question is still open.

Still open: blockers

Blocker 2: the random fallback is still broken for text-only LLMs.

  • Multimodal runs without --calib-samples are now rejected up front (dataset_lookup.py:101-105). That part is fixed.
  • The LLM path was not changed. With no --calib-* source, DefaultCalibrationDataAdapter returns {TEXT_DECODER: [(ids, mask), ...]}, a plain list (default_calibration_data_adapter.py:107-113).
  • LLMQuantizerAdapter.calibrate still raises ValueError("Calibration requires a corpus-backed DataLoader…") on anything that isn't a DataLoader (llm_quantizer_adapter.py:113-118).
  • That check runs only after export, prepare and observer initialization of every graph.
  • The reply says random fallback is "only supported for Llama", but no path on this commit actually supports it.
  • The random path also still ignores --max-seq-len: executorch_quantization_strategy.py:220 passes no extra_options, so it always uses the 1024 default.
  • Fix: either reject the run in get_calibration_dataset_adapter when no source is given (as legacy build_calib_dataloaders does), or make LLMQuantizerAdapter.calibrate accept the tuple list. Then drop "random fallback data" from the PR description.
  • Fix this →

Blocker 3: model names are normalized only inside model_lookup.

  • Each lookup helper now lowercases the name, which fixes the LLM_VARIANT_ARCHS and gemma4 branches.
  • But args.model itself is never normalized. LLMLoaderAdapter.load_tokenizer copies it into control_args.decoder_model (llm_loader_adapter.py:291, and the same in mllm_loader_adapter.py:413). TokenizerWrapper then compares that value as-is:
    • {"stories110m","stories260k"} (tokenizer.py:87)
    • "gemma4-e2b" (:138)
    • ALM_SPECIAL_TOKENS / VLM_SPECIAL_TOKENS (:280-292)
  • So --model Stories260K or --model InternVL3_1B still loads the wrong tokenizer path or misses its special tokens, with no error.
  • PipelineContext.model_name also carries the raw string.
  • The one-line fix from the original review still applies: set args.model = args.model.lower() in main(), or use choices=get_supported_models(). With that in place, the per-function .lower() calls are redundant.
  • Fix this →

Blocker 4 (stage hand-off) is deferred to A2. That's acceptable if the merge order is pinned. The bundle shape also changed in this commit: encoder bundles now carry {"io_type"} only. Whichever of B1/B2/A2 consumes quant_io_dtypes must handle a missing kv_type, so please note that in B1/B2.

New in the fix commit

  • cli.py:198-203: --soc lost its default but isn't required=True.
    • Without --soc, PipelineContextBuilder.build() raises "missing soc_model" (pipeline_context.py:99).
    • That happens only after _prepare_configs has already resolved model arch, transforms and state-dict loaders.
    • The resulting error goes through the generic "Pipeline failed" handler.
    • Fix: add required=True, or restore DEFAULT_SOC_MODEL.
  • model_lookup.py:236-237: stale comment. The "32 is the sentinel for unquantized KV IO" comment remains, but the if quant_recipe else 32 guard it describes is gone. get_model_arch / get_model_arch_config now raise when there's no recipe.
  • E3 ("dtype-driven quantizer when no recipe") can't run for the decoder.
    • get_model_arch now raises ValueError when quant_recipe is None (model_lookup.py:200-204).
    • So a decoder never reaches the no-recipe branch of _make_quantizer. Only tok_embedding uses it.
    • Fix: say so in the _make_quantizer log message and docstring, or drop the claim from the reply.

Partly addressed

Item Status
E1, tok_embedding quantizer Partly fixed. The make_quantizer defaults now match legacy (per_channel_linear=True, MinMaxObserver). But tok_embedding bundles still get quant_io_dtypes=None because there is no recipe, while legacy tags them with the decoder's kv/io dtypes (llm_wrappers.py:862-869).
E2, prefill override Partly fixed. The decoder prefill override is now gated on get_use_kv_cache. Legacy also gates the prefill tok_embedding override on the same flag (llm_wrappers.py:1117-1132). The new tok_embedding loop (executorch_quantization_strategy.py:612-619) still overrides TOK_EMBEDDING_GRAPH_NAMES unconditionally.
psiddh #3, unused CLI flags Mostly fixed. The train/eval/temperature/media/pre-gen flags are removed. Still advertised but unused: --max-context-len, which is still accepted and then overwritten (cli.py:235-240, :384); --prompt; --compile-only, which only logs; and --use-fp16, which runs model prep only and produces nothing. Remove --max-context-len too, or reject it when it differs from --max-seq-len.
psiddh #1, train/eval adapters Deferred. The CLI no longer wires them, but get_training_dataset_adapter / get_eval_dataset_adapter are still exported from datasets/__init__.py and still read only the calib_* options. Either drop them from __all__ until QAT/eval lands, or add a TODO.
H2, CLI namespace mutation Deferred, but it's the root of Blocker 3 above.

Testing gaps in the fix commit

The new behavior is mostly untested. The only additions are GraphBundle unit tests and the prefill-gating mock tests. Missing:

  • _get_quant_io_dtypes with a real encoder recipe (e.g. InternVL3EncoderQuantRecipe) through invoke()
  • the MLLM rejections in dataset_lookup.py
  • the new quant_recipe is None ValueError
  • the _validate_input GRAPH_FORWARD invariant
  • the new make_quantizer defaults
  • any cli.py test, which would have caught the --soc and mixed-case issues

Verified as addressed

  • Blocker 1: GraphBundle accepts {"io_type"}.
  • E5: MLLM calibration rejects --calib-tasks / --calib-hf-dataset.
  • H4: embedding_quantize is now read from model_options.
  • psiddh Re-sync with internal repository #2: a None recipe now raises.
  • psiddh Add unlifting pass under private config #4: the GRAPH_FORWARD invariant is now checked in _validate_input.
  • psiddh [build] Add BUCK.oss #5: the break was added in _calibrate.
  • psiddh Re-sync with internal repository #6: quant_utilities now uses get_attr_from_target / set_attr_from_target from builders/utils.py.
  • Code quality: the bad __all__ entries are gone, model_lookup imports are lazy, the stale genai_pipeline.py docstring is removed, chat_template is typed as a callable, and the export_tokenizer docstrings now say output_dir is ignored.
  • Accepted as follow-ups per the author's reply (no change expected here): A (dependency on examples/), B (parity), C (registry-driven logic), D (strategy/adapter split), F (module selection), G (default adapters), H1 / H3 (extra_options and getattr probing). Please track A and B in the LLD, because Phase 3 deletion depends on them.

Recommendation

Request Changes.

  • Must fix: Blockers 2 and 3 (each is a small change) and the --soc regression.
  • Everything else can be a follow-up, as long as it's tracked.

  Require explicit calibration sources in the CLI and dataset factory, normalize
  model names before tokenizer setup, and validate the target SoC before model
  preparation. Preserve adapter-agnostic calibration strategy behavior while
  forwarding resolved calibration options to adapters. Clarify quantization
  metadata and add focused regression coverage for model lookup and graph
  quantization behavior.

  Co-Authored-By: Claude <noreply@anthropic.com>
@DannyYuyang-quic

DannyYuyang-quic commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

Claude finished @psiddh's task in 5m 7s —— View job

Summary: Most of the feedback is addressed in 55cc8f6, but two of the three original blockers are only partly fixed: an LLM run with no --calib-* source still fails late, and mixed-case --model names still reach TokenizerWrapper unnormalized. The fix commit also adds one small regression: --soc lost its default but isn't marked required.

Re-review of #23050: status of every earlier finding and inline comment

Click to view review responses # PR A1 review responses

Blocker 2: calibration without an explicit source

Fixed. Quantized CLI runs now fail during argument validation when no
calibration source is supplied. Users must provide --calib-tasks,
--calib-samples, or --calib-hf-dataset; this avoids silently producing
quantization results from synthetic data that may not represent the target
workload.

For multimodal models, get_dataset_adapter() is the single owner of source
validation: it accepts --calib-samples and rejects --calib-tasks or
--calib-hf-dataset. The calibration factory retains the corresponding
source-required invariant for callers that construct adapters through that
factory; direct strategy construction remains adapter-agnostic.

This removes the random-fallback route from the CLI and calibration-factory
path. DefaultCalibrationDataAdapter remains the strategy-level default for
direct construction and isolated tests; the strategy consumes its injected
adapter without owning source-selection policy. It now forwards the resolved
context options through the calibration-adapter protocol, so an explicit
random-adapter path uses max_context_len rather than its independent 1024
default.

Blocker 3: mixed-case model names

Fixed. The CLI canonicalizes args.model to lowercase immediately after
validating that --model was supplied. This ensures that PipelineContext,
the legacy decoder_model bridge, tokenizer selection, and special-token
handling all receive the registry key rather than a mixed-case CLI spelling.

The defensive lowercase normalization in model_lookup.py remains
intentionally. Those helpers can be called independently of cli.py, and
should preserve a case-insensitive reusable lookup contract.

Blocker 4: stage hand-off

Deferred to A2. Encoder bundles carry an io_type-only
quant_io_dtypes mapping rather than decoder KV metadata. Any B1/B2/A2
consumer must tolerate an encoder bundle without kv_type. The A2 integration
notes track the required multi-graph bundle hand-off and compilation input
contract.

--soc parser regression

Fixed. --soc remains optional at argparse level so --list-models works
without a target. Normal pipeline execution now rejects a missing --soc at
the beginning of _validate_args(), before configuration, transforms, or
state-dict loader setup.

Gemma4 stale KV-width comment

Fixed. Removed the comment after the fallback policy had already changed
in the preceding fix commit. The current pipeline requires a decoder recipe
during architecture construction to configure its KV-cache IO width, so the
comment describing the former else 32 behavior no longer matched the active
code path.

E3: dtype-driven quantizer without a recipe

Clarification. The supported model registry does not contain a decoder
with quant_recipe=None; every registered decoder model has a concrete recipe
class. The decoder guard is therefore not reached by a normal supported CLI
model.

The guard applies to future/custom registry entries whose decoder recipe is
literally None. It runs during model architecture preparation because the
current decoder construction requires a KV-cache IO bit width, before quantizer
creation and before --use-fp16 selects stages.

This is distinct from the supported component-level no-recipe path. Multimodal
token embedding intentionally has no recipe, receives a dtype-driven QNN
quantizer, and leaves recipe-derived graph IO tags unset. A malformed recipe
class without default_quant_dtype is another distinct case; it fails under
the existing recipe-base invariant at recipe construction time.

The log message now says dtype-driven rather than implying that a recipe is
merely "not set yet." The implementation is deliberately not documented as
specific to token embedding because other components may legitimately use the
same component-level contract.

E1: token-embedding IO dtype metadata

Clarification. tok_embedding.quant_io_dtypes=None is intentional. Token
embedding has no KV-cache boundary, and the pipeline derives boundary tags from
the component's recipe only. The multimodal embedding component deliberately
has no recipe, so no recipe-derived IO mapping is produced. This does not imply
that decoder KV metadata should be copied to the embedding graph.

Added focused coverage for an encoder-style recipe that produces the supported
{"io_type": ...} mapping without kv_type.

E2: prefill token-embedding encoding override

No change required. get_use_kv_cache is a decoder-specific topology
condition: decoder reconciliation copies cache-output encodings to cache-input
placeholders. Token embedding has no KV-cache placeholders, and its override
uses normal encoding propagation without cache-layer handling.

Legacy nested the embedding override below the decoder condition because, in
the earlier PTQ flow, no-cache prefill was directly calibrated while
cache-bearing prefill received a dummy observer pass and borrowed decode
encodings. That historical nesting is not an embedding dependency on KV cache.

The pipeline intentionally propagates calibration embedding encodings to every
deployed embedding variant, including no-cache prefill, while retaining the
cache-topology gate for decoder reconciliation. A focused strategy test now
covers this separation.

CLI flags

  • --max-context-len: Fixed. It is now honored when explicitly supplied.
    During the current pre-attention-sink transition, its resolved value is
    mirrored into max_seq_len so legacy graph/config consumers use one shape.
    If absent, the existing --max-seq-len value remains the compatibility input
    for both fields.
  • --max-seq-len: Retained. It remains the compatibility shape input until
    runtime and attention-sink support introduce independent sequence and context
    semantics.
  • --compile-only: Deferred to A2. This follow-up leaves the existing CLI
    surface unchanged. Its effective stage-selection semantics will be finalized
    with the compilation and inference integration; the current CLI does not yet
    install either stage.
  • --prompt: Retained. It is a forward-compatible PipelineContext input;
    the current CLI does not yet install an active inference stage that consumes
    it.
  • --use-fp16: Retained. It currently skips construction of the
    quantization stage only. It does not by itself provide a complete FP16
    artifact flow: model dtype remains controlled by --dtype-override, and
    compilation is deferred to A2.

Train/eval adapters

Deferred and tracked for the QAT/evaluation integration. The exported
training and evaluation adapter APIs are not wired into the current native PTQ
flow, and their current factories do not consume the advertised train_* or
eval_* source fields. That purpose-specific source-selection contract will be
defined with the QAT/evaluation integration rather than pre-committing the
purpose-agnostic calibration loader helper to a future shape.

Testing scope

Added focused tests for the boundaries changed or clarified in this review:

  • model lookup covers the reusable case-insensitive registry contract and the
    normal architecture-construction error for a decoder without a recipe;
  • quantization strategy covers io-type-only boundary metadata, no-cache prefill
    embedding encoding propagation, and calibration adapter option forwarding.

Dataset source-selection policy remains covered at the factory boundary in this
change, but a broader loader-routing matrix is deferred with the evolving
purpose-specific dataset API. I am not adding a patched-run_pipeline CLI
plumbing test here. Model
lookup remains independently case-insensitive, and context/model-preparation
validation owns required execution fields. A CLI test layer should be added
with the PR that finalizes the public pipeline invocation contract, including
compilation and inference stage composition; this avoids freezing a temporary
bridge through mock-only tests.

_validate_input invariant

Fixed. Removed the dead GRAPH_FORWARD deployment check. The static
decoder and token-embedding deployment-name sets do not contain
GRAPH_FORWARD, so the condition could not fire. No regression test was added
for an unreachable branch.

@psiddh

psiddh commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Can you check if the failing CI job ? Is it related
[332/332] importing executorch.backends.qualcomm.utils.utils
1/332 QNN import failure(s):
FAIL: executorch.backends.qualcomm.genai_pipeline.model_lookup -- ModuleNotFoundError: No module named 'lm_eval'

@DannyYuyang-quic

Copy link
Copy Markdown
Contributor Author

Can you check if the failing CI job ? Is it related [332/332] importing executorch.backends.qualcomm.utils.utils 1/332 QNN import failure(s): FAIL: executorch.backends.qualcomm.genai_pipeline.model_lookup -- ModuleNotFoundError: No module named 'lm_eval'

model_lookup.py was pulling in the legacy wrapper stack at import time, which eventually imports the optional
lm_eval package. Since the QNN import CI does not install lm_eval, the smoke test failed; I moved those imports
inside the functions that actually need them.

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