File size: 9,606 Bytes
83894cd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 | # Copyright 2026 Apple Inc.
#
# Use of this source code is governed by a BSD-3-clause license that can
# be found in the LICENSE file or at https://opensource.org/licenses/BSD-3-Clause
"""
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__)
# Composite ops that are externalized (kept as named composites in the MLIR graph
# rather than being inlined/decomposed).
_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)
)
# Clamp `max_position_embeddings` so KVCache.create_cache_tensors doesn't
# allocate a full-context cache for huge models
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 the caller didn't pass input_names explicitly, derive them from
# ``reference_inputs.keys()`` while excluding any name the caller declared
# as state. This keeps the call to ``add_pytorch_module`` predictable
# regardless of whether ``state_names`` is also set.
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)
# Determine target dtype from the model parameters
target_dtype = next(model.parameters()).dtype
logger.info(
f"Exporting macOS model (dtype={target_dtype}, max_context_length={max_context_length})"
)
# Models with extra mutable state (e.g. LFM2's conv state) provide their own
# reference inputs + state names; everything else uses the KV-only defaults.
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
|