Skip to content
Open
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
1 change: 1 addition & 0 deletions modelopt/torch/export/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from .model_utils import *
from .moe_utils import *
from .plugins import *
from .registry import *
from .transformer_engine import *
from .unified_export_hf import *
from .unified_export_megatron import *
148 changes: 148 additions & 0 deletions modelopt/torch/export/registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Registry dispatching per-module export logic for the unified HF export path.

This mirrors the registration-and-dispatch idiom of
:class:`QuantModuleRegistry <modelopt.torch.quantization.nn.modules.quant_module.QuantModuleRegistry>`,
but not its mechanism: quantization registers replacement classes and converts modules
in place, whereas export registers :class:`ModuleExporter` handlers that emit compressed
weights and scale buffers for a module without changing its class.

Handlers are matched per module during the export walk. Registering a handler for a new
module type replaces what previously required editing if/elif chains inside
``unified_export_hf.py``.
"""

from collections.abc import Callable
from dataclasses import dataclass, field

import torch
import torch.nn as nn

__all__ = ["ExportContext", "ExportModuleRegistry", "ModuleExporter"]


@dataclass
class ExportContext:
"""Shared state for a single export invocation, passed to every handler call.

The tied-weight dedup caches must be scoped to one export invocation: a
process-global cache would carry stale entries whose ``data_ptr`` keys can be
recycled by PyTorch's allocator across exports, causing silent false-positive
aliasing. ``tied_cache`` (int keys) holds dense Linear / per-expert wrapper
dedup; ``moe_tied_cache`` (tuple keys) holds MoE fused-experts module dedup.
"""

model: nn.Module
dtype: torch.dtype
is_modelopt_qlora: bool = False
tied_cache: dict[int, nn.Module] = field(default_factory=dict)
moe_tied_cache: dict[tuple[int, int], nn.Module] = field(default_factory=dict)


class ModuleExporter:
"""Base class for per-module export handlers.

Subclasses are registered on :data:`ExportModuleRegistry` and dispatched during the
export walk. Both hooks default to no-ops so a handler only implements the phases
that apply to its module type.
"""

def prepare_moe_inputs(self, name: str, moe_module: nn.Module, ctx: ExportContext) -> None:
"""Fill missing expert input-quantizer amax values before fusion and compression.

Called once per MoE block whose ``.experts`` container matched this entry.
``moe_module`` is the MoE block itself, not the experts container.
"""

def export(self, name: str, module: nn.Module, ctx: ExportContext) -> None:
"""Emit compressed weights and scale buffers for ``module``, in place."""


class _ExportModuleRegistryCls:
"""Ordered, first-match-wins registry mapping modules to :class:`ModuleExporter`.

An entry can match a module by any combination of:

- a class key: the registered class appears in ``type(module).__mro__``, so
dynamically generated quantized classes (e.g. ``QuantLinear``) match through
their original base class;
- a class-name string key: the string equals the ``__name__`` of a class in the
MRO — for classes that cannot be imported statically (trust_remote_code models
or on-the-fly generated quantized classes);
- a predicate on the module instance, for structural detection.

When keys and a predicate are both given, both must match. Entries are tried in
registration order and the first match wins, so more specific handlers must be
registered before generic ones. The built-in handlers end with broad structural
catch-alls (e.g. any iterable module), so external handlers should register with
``prepend=True`` to take precedence over them.
"""

def __init__(self) -> None:
self._entries: list[
tuple[tuple[type | str, ...], Callable[[nn.Module], bool] | None, ModuleExporter]
] = []

def register(
self,
*keys: type | str,
predicate: Callable[[nn.Module], bool] | None = None,
prepend: bool = False,
):
"""Return a decorator registering a :class:`ModuleExporter` subclass.

Re-registering the same exporter class (e.g. on module reload) replaces its
existing entry in place instead of appending a duplicate.

Usage::

@ExportModuleRegistry.register("Llama4TextExperts", "GptOssExperts")
class _BmmExpertsExporter(ModuleExporter): ...
"""
assert keys or predicate is not None, "register() requires at least one key or a predicate"

def decorator(exporter_cls: type[ModuleExporter]) -> type[ModuleExporter]:
entry = (keys, predicate, exporter_cls())
identity = (exporter_cls.__module__, exporter_cls.__qualname__)
for i, (_, _, existing) in enumerate(self._entries):
if (type(existing).__module__, type(existing).__qualname__) == identity:
self._entries[i] = entry
return exporter_cls
if prepend:
self._entries.insert(0, entry)
else:
self._entries.append(entry)
return exporter_cls

return decorator

def match(self, module: nn.Module) -> ModuleExporter | None:
"""Return the first registered exporter matching ``module``, or None."""
mro = type(module).__mro__
mro_names = {cls.__name__ for cls in mro}
for keys, predicate, exporter in self._entries:
if keys and not any(
key in mro_names if isinstance(key, str) else key in mro for key in keys
):
continue
if predicate is not None and not predicate(module):
continue
return exporter
return None


ExportModuleRegistry = _ExportModuleRegistryCls()
Loading
Loading