| |
| |
| |
| |
|
|
| """ |
| macOS model export pipeline. |
| |
| Exports a PyTorch LLM model to a Core AI AIProgram via: |
| torch.export -> decompose -> defunctionalize -> TorchConverter -> optimize. |
| """ |
|
|
| import logging |
|
|
| import coreai_torch |
| import coreai_torch.composite_ops |
| import torch |
| from coreai.authoring import AIProgram |
|
|
| from coreai_models.export._constants import ( |
| KEY_CACHE_NAME, |
| QUANT_TRACE_OFFSET, |
| QUANT_TRACE_QUERY_LEN, |
| TRACE_KV_CACHE_SEQ_LEN, |
| VALUE_CACHE_NAME, |
| ) |
| from coreai_models.export.mlir_ops import ( |
| register_custom_torch_lowering, |
| remove_functionalization, |
| ) |
| from coreai_models.primitives.macos.cache import KVCache |
|
|
| logger = logging.getLogger(__name__) |
|
|
| |
| |
| _EXTERNALIZE_SPECS = [ |
| coreai_torch.ExternalizeSpec( |
| target_class=coreai_torch.composite_ops.GatherMM, |
| composite_op_name="gather_mm", |
| composite_attrs=["num_batch_axes"], |
| ), |
| coreai_torch.ExternalizeSpec( |
| target_class=coreai_torch.composite_ops.RMSNormImpl, |
| composite_op_name="rms_norm", |
| composite_attrs=["axes", "eps"], |
| ), |
| coreai_torch.ExternalizeSpec( |
| target_class=coreai_torch.composite_ops.RoPE, |
| composite_op_name="rope", |
| composite_attrs=["scale", "base", "dims", "interleaved"], |
| ), |
| coreai_torch.ExternalizeSpec( |
| target_class=coreai_torch.composite_ops.SDPA, |
| composite_op_name="scaled_dot_product_attention", |
| composite_attrs=["scale", "is_causal", "window_size"], |
| ), |
| coreai_torch.ExternalizeSpec( |
| target_class=coreai_torch.composite_ops.GatedDeltaUpdate, |
| composite_op_name="gated_delta_update", |
| composite_attrs=[], |
| ), |
| ] |
|
|
|
|
| def _build_reference_inputs( |
| model: torch.nn.Module, |
| config, |
| target_dtype: torch.dtype, |
| max_context_length: int, |
| ) -> tuple[dict[str, torch.Tensor], dict]: |
| """Build reference inputs and dynamic shapes for macOS model export. |
| |
| Args: |
| model: The PyTorch model (used only to read config). |
| config: HuggingFace model config. |
| target_dtype: Data type for cache tensors. |
| max_context_length: Maximum context length for the model. |
| |
| Returns: |
| Tuple of (reference_inputs dict, dynamic_shapes dict). |
| """ |
| batch_size = 1 |
| vocab_size = config.vocab_size |
|
|
| input_ids = torch.randint(1, vocab_size, (batch_size, QUANT_TRACE_QUERY_LEN), dtype=torch.int32) |
| position_ids = ( |
| torch.arange(QUANT_TRACE_QUERY_LEN + QUANT_TRACE_OFFSET, dtype=torch.int32) |
| .unsqueeze(0) |
| .expand(batch_size, QUANT_TRACE_QUERY_LEN + QUANT_TRACE_OFFSET) |
| ) |
|
|
| |
| |
| saved_max_pos = config.max_position_embeddings |
| config.max_position_embeddings = TRACE_KV_CACHE_SEQ_LEN |
| k_cache, v_cache = KVCache.create_cache_tensors(config, dtype=target_dtype) |
| config.max_position_embeddings = saved_max_pos |
|
|
| reference_inputs = { |
| "input_ids": input_ids, |
| "position_ids": position_ids, |
| "k_cache": k_cache, |
| "v_cache": v_cache, |
| } |
|
|
| dynamic_shapes = { |
| "input_ids": {1: torch.export.Dim("seq_ids", max=max_context_length - 2)}, |
| "position_ids": { |
| 1: torch.export.Dim("seq_pos", min=QUANT_TRACE_QUERY_LEN, max=max_context_length - 1) |
| }, |
| "k_cache": { |
| KVCache.seq_len_dim(): torch.export.Dim( |
| "k_seq_len", min=TRACE_KV_CACHE_SEQ_LEN, max=max_context_length |
| ) |
| }, |
| "v_cache": { |
| KVCache.seq_len_dim(): torch.export.Dim( |
| "v_seq_len", min=TRACE_KV_CACHE_SEQ_LEN, max=max_context_length |
| ) |
| }, |
| } |
|
|
| return reference_inputs, dynamic_shapes |
|
|
|
|
| def export_to_coreai( |
| model: torch.nn.Module, |
| reference_inputs: dict[str, torch.Tensor], |
| dynamic_shapes: dict | None = None, |
| input_names: tuple[str, ...] | None = None, |
| output_names: tuple[str, ...] | None = None, |
| state_names: tuple[str, ...] | None = None, |
| ) -> AIProgram: |
| """Export a stateful macOS model to a AIProgram. |
| |
| Low-level building block under `export_macos_model` (text-only LLMs). Use |
| that when possible; reach for this directly only when you need |
| component-specific input/output names that `export_macos_model`'s |
| text-only defaults don't fit. |
| |
| This is the core export function that handles: |
| 1. torch.export with no_grad |
| 2. Decomposition via coreai_torch decomp table |
| 3. Defunctionalization (replacing auto-functionalized ops with immutable variants) |
| 4. TorchConverter with externalized composite modules |
| 5. Custom MLIR lowering registration |
| |
| Args: |
| model: The PyTorch model to export (must be in eval mode). |
| reference_inputs: Dict of reference input tensors (keyword args to forward). |
| dynamic_shapes: Dynamic shape specifications for torch.export. |
| input_names: Names for the model inputs in the exported graph. If both |
| ``input_names`` and ``state_names`` are ``None``, the names default |
| to ``reference_inputs.keys()``. |
| output_names: Names for the model outputs in the exported graph. |
| state_names: Names of inputs that are state (i.e. mutated in place by |
| the forward pass and surfaced via the runtime ``state=`` kwarg |
| rather than as regular inputs/outputs). |
| |
| Returns: |
| A AIProgram ready for optimization and compilation. |
| """ |
| |
| |
| |
| |
| if input_names is None: |
| state_names_set = set(state_names or ()) |
| input_names = tuple(k for k in reference_inputs if k not in state_names_set) |
|
|
| def export_fn(module: torch.nn.Module) -> torch.export.ExportedProgram: |
| with torch.no_grad(): |
| aten_exported_program = torch.export.export( |
| module, |
| args=(), |
| kwargs=reference_inputs, |
| dynamic_shapes=dynamic_shapes, |
| ) |
| coreai_decomp_table = coreai_torch.get_decomp_table() |
| coreaten_exported_program = aten_exported_program.run_decompositions(coreai_decomp_table) |
| remove_functionalization(coreaten_exported_program) |
| return coreaten_exported_program |
|
|
| model.eval() |
| converter = coreai_torch.TorchConverter() |
| converter.add_pytorch_module( |
| model, |
| export_fn=export_fn, |
| externalize_modules=_EXTERNALIZE_SPECS, |
| input_names=input_names, |
| output_names=output_names, |
| state_names=state_names, |
| ) |
| register_custom_torch_lowering(converter) |
| return converter.to_coreai() |
|
|
|
|
| def export_macos_model( |
| model: torch.nn.Module, |
| config, |
| export_config, |
| ) -> AIProgram: |
| """Export a macOS model to a AIProgram. |
| |
| This is the main entry point for macOS model export. It: |
| 1. Builds reference inputs and dynamic shapes from the model config |
| 2. Exports the model through torch.export -> TorchConverter |
| 3. Optimizes the resulting AIProgram |
| |
| Args: |
| model: A loaded PyTorch model (already in the correct dtype). |
| config: HuggingFace model config (used for cache dimensions, vocab size, etc.). |
| export_config: An ExportConfig instance (used for max_context_length, etc.). |
| |
| Returns: |
| An optimized AIProgram ready for MLIR quantization and compilation. |
| """ |
| max_context_length = getattr(export_config, "max_context_length", None) |
| if max_context_length is None: |
| max_context_length = getattr(config, "max_position_embeddings", 2048) |
|
|
| |
| target_dtype = next(model.parameters()).dtype |
|
|
| logger.info( |
| f"Exporting macOS model (dtype={target_dtype}, max_context_length={max_context_length})" |
| ) |
|
|
| |
| |
| if hasattr(type(model), "build_reference_inputs"): |
| reference_inputs, dynamic_shapes = type(model).build_reference_inputs( |
| config, |
| target_dtype, |
| max_context_length, |
| QUANT_TRACE_QUERY_LEN, |
| QUANT_TRACE_OFFSET, |
| TRACE_KV_CACHE_SEQ_LEN, |
| ) |
| else: |
| reference_inputs, dynamic_shapes = _build_reference_inputs( |
| model, config, target_dtype, max_context_length |
| ) |
|
|
| input_names = ("input_ids", "position_ids") |
| output_names = ("logits",) |
| if hasattr(type(model), "state_names"): |
| state_names = tuple(type(model).state_names()) |
| else: |
| state_names = (KEY_CACHE_NAME, VALUE_CACHE_NAME) |
|
|
| logger.info("Exporting model to Core AI dialect...") |
| coreai_program = export_to_coreai( |
| model, |
| reference_inputs, |
| dynamic_shapes=dynamic_shapes, |
| input_names=input_names, |
| output_names=output_names, |
| state_names=state_names, |
| ) |
|
|
| logger.info("Optimizing AIProgram...") |
| coreai_program.optimize() |
|
|
| return coreai_program |
|
|