Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional, TYPE_CHECKING
from typing import Any, List, Optional, Tuple, TYPE_CHECKING

if TYPE_CHECKING:
from executorch.backends.qualcomm.serialization.qc_schema import (
Expand All @@ -23,16 +23,29 @@
class CompilationInputConfig:
"""Input configuration for the compilation stage.

``model`` and ``example_inputs`` are ``Optional`` only because the
orchestrator builds this from the previous stages' output, which is empty
when those stages are skipped. **Both are required once the compilation
stage executes**, and strategies should validate their presence.

Attributes:
soc_model: The target SoC (e.g., QcomChipset.SM8750). Required.
backend_type: QNN backend type (HTP, GPU, LPAI, etc.). Required.
model: The nn.Module to compile (quantized or original for FP16 mode).
Required when the stage runs.
example_inputs: Positional example inputs for ``torch.export``. Required
when the stage runs. Sourced from the **model** via
``ModelLoaderAdapter.get_example_inputs``, never from calibration
data: this tuple defines the exported graph's positional signature,
supplies the zero-initialized KV caches a dataset sample does not
carry, and fixes the AR length because HTP has no dynamic shapes.
artifact_dir: Directory to store compiled artifacts.
compile_specs: QNN compiler specifications for backend delegation.
"""

soc_model: "QcomChipset"
backend_type: "QnnExecuTorchBackendType"
model: Optional["nn.Module"] = None
example_inputs: Optional[Tuple[Any, ...]] = None
artifact_dir: Path = field(default_factory=lambda: Path("."))
compile_specs: Optional[List["CompileSpec"]] = None
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ class InferenceOutputConfig:
"""Output produced by the inference stage.

Attributes:
inference_results: Generated text output(s) from the model.
inference_results: Generated text output(s) from the model, or the paths
of the pulled result files when the adapter writes results to disk
rather than returning them. Stays ``List[str]`` rather than widening
to ``List[Any]``: ``InferenceResult.output_data`` is already decoded
text by contract, so it needs no conversion, and coercing it in the
strategy would risk stringifying raw token ids.
performance_metrics: Performance data (e.g., TTFT, tokens/sec).
eval_results: Evaluation metric results (e.g., SQNR, perplexity).
etdump: Optional ETDump for debugging. ExecuTorch engine only.
Expand Down
30 changes: 30 additions & 0 deletions backends/qualcomm/genai_pipeline/genai_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,34 @@ def _resolve_stage(
stage_name: str,
skip: set,
):
"""Build one stage from the registry, or ``None`` if it is skipped.

Args:
engine_proxy: Routes each stage to its engine.
stage_name: The stage to resolve.
skip: Stage names to skip entirely.

Returns:
The constructed stage, or ``None`` when the stage is skipped.

Raises:
ValueError: If no strategy is registered for the stage's engine.

.. note::
Strategies are constructed with **no arguments**, so each gets its
default adapter. That suffices for model preparation and
quantization, but not for compilation or inference: the default
compiler adapter has no body yet, and the inference strategy
requires an injected device runner. A pipeline built purely from the
registry therefore cannot reach a working adapter for those two
stages.

Injecting adapters today means constructing ``GenAIPipeline`` and
the stages directly rather than going through ``from_proxy``.
Threading adapters through this path lands together with the adapter
bodies, so the hook is shaped by what its callers actually need
rather than guessed at now.
"""
if stage_name in skip:
return None

Expand Down Expand Up @@ -241,6 +269,8 @@ def _run_compilation(
soc_model=context.soc_model,
backend_type=self._engine_proxy.backend_type,
model=model,
# Export inputs come from the model, not from calibration_data.
example_inputs=model_prep_output.example_inputs,
artifact_dir=Path(context.artifact_dir),
)
output = self._compilation_stage.invoke(context, input_config)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Protocol, runtime_checkable
from typing import Any, Dict, List, Optional, Protocol, runtime_checkable, Tuple


@dataclass
Expand All @@ -28,8 +28,10 @@ class CompilationResult:
class CompilerAdapter(Protocol):
"""Protocol for compilation operations.

Wraps external compilation APIs (ExportSession, to_edge_transform_and_lower_to_qnn)
behind an injectable interface for testability.
Wraps ``to_edge_transform_and_lower_to_qnn`` behind an injectable interface
for testability. The parameter list of ``compile_model`` deliberately
mirrors that function so that no required lowering input has to travel as a
string-keyed option.

.. note::
``compile_model`` lowers a **single graph**, mirroring the underlying
Expand All @@ -47,23 +49,43 @@ class CompilerAdapter(Protocol):
def compile_model(
self,
model: Any,
example_inputs: Tuple[Any, ...],
compile_specs: Any,
artifact_dir: Path,
file_name: str,
soc_model: Any,
backend_type: Any,
constant_methods: Optional[Dict[str, Any]] = None,
dep_table: Optional[Dict] = None,
passes_job: Optional[Any] = None,
extra_options: Optional[Dict[str, Any]] = None,
) -> CompilationResult:
"""Compile the model to on-device .pte artifacts.

The parameter list mirrors ``to_edge_transform_and_lower_to_qnn``: every
argument that lowering genuinely needs is explicit, and
``extra_options`` is reserved for optional tuning knobs. Passing a
required input as a string-keyed option is deliberately avoided -- it
hides the contract and fails at runtime rather than at the call site.

Args:
model: The model to compile (nn.Module or quantized model).
example_inputs: Positional example inputs for ``torch.export``,
sourced from the model itself.
compile_specs: QNN compiler specifications for backend delegation.
artifact_dir: Directory to store compiled artifacts.
file_name: Base name for the output .pte file.
soc_model: Target SoC chipset.
backend_type: QNN backend type (HTP, GPU, LPAI).
extra_options: Additional compilation options.
constant_methods: Methods returning constants in eager mode. For a
decoder this carries the quantization attributes written during
quantization, so it is only complete after that stage.
dep_table: Per-graph pass dependency table.
passes_job: Per-graph pass configuration.
extra_options: Optional tuning knobs (``skip_node_id_set``,
``skip_node_op_set``, ``skip_mutable_buffer``,
``convert_linear_to_conv2d``, ``generate_etrecord``,
``executorch_backend_config``).

Returns:
CompilationResult with artifact paths and optional etrecord. May hold
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,68 +6,95 @@

from __future__ import annotations

import logging
from pathlib import Path
from typing import Any, Dict, Optional
from typing import Any, Dict, Optional, Tuple

from executorch.backends.qualcomm.genai_pipeline.strategies.compilation.compiler_adapter import (
CompilationResult,
)

logger = logging.getLogger(__name__)


class DefaultCompilerAdapter:
"""Default adapter delegating to the ExportSession recipe-based pipeline.
"""Default adapter delegating to ``to_edge_transform_and_lower_to_qnn``.

.. note::
The body is deliberately unimplemented **in this PR only**: this PR
establishes the adapter interfaces, and the compilation strategy that
drives this adapter lands in the following PR, so the implementation
ships alongside its caller rather than ahead of it.
The signature below is the final one -- it mirrors
``to_edge_transform_and_lower_to_qnn`` argument-for-argument, so the
per-graph inputs (``compile_specs``, ``dep_table``, ``passes_job``,
``constant_methods``) are explicit parameters rather than
``extra_options`` keys.

The **body** is not implemented in this PR. Lowering is implementation
work rather than interface work, and the version this package needs is
the multi-graph one: ``to_edge_transform_and_lower_to_qnn`` accepts
graph-name-keyed dicts for ``module`` / ``inputs`` / ``compiler_specs``
/ ``dep_table`` / ``passes_job``, and a hybrid decoder groups its graphs
into a single multi-method ``.pte`` for weight sharing. Writing a
single-graph body here and then replacing it would mean implementing the
lowering twice, so it lands with the strategy-level fan-out that calls
it.

The APIs it will delegate to -- ``ExportRecipe.get_recipe`` with
``QNNRecipeType.FP16`` and ``ExportSession`` -- are already available
in-tree; nothing external is blocking it. Until the follow-up lands,
inject a custom ``CompilerAdapter`` implementation.
A recipe-based body (``ExportRecipe.get_recipe(QNNRecipeType.FP16)`` +
``ExportSession``) was considered and rejected: ``QNNRecipeProvider``
accepts only ``soc_model`` and the three ``skip_*`` keys and silently
ignores the rest, so ``dep_table``, ``passes_job``, ``constant_methods``
and ``convert_linear_to_conv2d`` are unreachable through it, and it
hardcodes ``use_fp16=True``.

Until the body lands, inject a custom ``CompilerAdapter``.
"""

def compile_model(
self,
model: Any,
example_inputs: Tuple[Any, ...],
compile_specs: Any,
artifact_dir: Path,
file_name: str,
soc_model: Any,
backend_type: Any,
constant_methods: Optional[Dict[str, Any]] = None,
dep_table: Optional[Dict] = None,
passes_job: Optional[Any] = None,
extra_options: Optional[Dict[str, Any]] = None,
) -> CompilationResult:
"""Compile the model using ExportSession.

Placeholder for the recipe-based compilation flow, which uses
``ExportRecipe.get_recipe(QNNRecipeType.FP16, ...)`` combined with
``ExportSession``. Implemented in the compilation-strategy PR.
"""Compile the model via ``to_edge_transform_and_lower_to_qnn``.

Args:
model: The model to compile (nn.Module or quantized model).
compile_specs: QNN compiler specifications (currently unused when
using recipe-based flow; kept for future custom compile spec support).
example_inputs: Positional example inputs for ``torch.export``,
sourced from the model itself.
compile_specs: QNN compiler specifications for backend delegation.
artifact_dir: Directory to store compiled .pte artifacts.
file_name: Base name for the output .pte file.
soc_model: Target SoC chipset enum value.
backend_type: QNN backend type (HTP, GPU, LPAI).
extra_options: Additional compilation options. Supported keys:
- ``example_inputs``: Sample inputs for torch.export.
- ``generate_etrecord``: Whether to generate ETRecord.
- ``constant_methods``: Dict of constant methods.
constant_methods: Methods returning constants in eager mode. For a
decoder this carries the quantization attributes written into
``meta`` during quantization, so it is only complete once that
stage has run.
dep_table: Per-graph pass dependency table.
passes_job: Per-graph pass configuration.
extra_options: Optional tuning knobs forwarded to lowering:
``skip_node_id_set``, ``skip_node_op_set``,
``skip_mutable_buffer``, ``convert_linear_to_conv2d``,
``generate_etrecord``, and ``executorch_backend_config`` to
override the ``to_executorch`` configuration.

Returns:
CompilationResult with artifact paths and optional etrecord.

Raises:
NotImplementedError: Always raised in this PR. The body lands with
the compilation strategy that drives it; inject a custom
CompilerAdapter until then.
NotImplementedError: Always raised in this PR; the body lands with
the multi-graph lowering that calls it.
"""
raise NotImplementedError(
"DefaultCompilerAdapter is not implemented in this PR: the body "
"lands together with the compilation strategy that drives it. "
"Inject a custom CompilerAdapter implementation until then."
"DefaultCompilerAdapter has no body yet: lowering is implemented "
"together with the strategy-level multi-graph fan-out that calls "
"it, so that to_edge_transform_and_lower_to_qnn is wired up once "
"in its graph-keyed form. Inject a custom CompilerAdapter until "
"then."
)
Loading
Loading