HuggingFace Hub Integration
===========================
xpm-torch models can be pushed to and loaded from the
`HuggingFace Hub `_ via
:class:`~xpm_torch.huggingface.TorchHFHub` (which extends experimaestro's
``ExperimaestroHFHub``). The serialization preserves the full experimaestro
configuration graph, so a downloaded model can be used directly in further
experiments.
Exporting models via actions
----------------------------
The recommended way to export trained models is through experimaestro's
**action system**. When a :class:`~xpm_torch.learner.Learner` is submitted,
it automatically registers :class:`~xpm_torch.actions.ExportAction` instances
for the last checkpoint and for each listener's best checkpoint. After the
experiment completes, these actions can be executed interactively via the
experimaestro CLI (``experimaestro experiments actions``).
:class:`~xpm_torch.actions.ExportAction` prompts the user to choose between
uploading to HuggingFace Hub or saving to a local directory, then delegates
to :class:`~xpm_torch.huggingface.TorchHFHub`.
How actions are registered
~~~~~~~~~~~~~~~~~~~~~~~~~~
Actions are registered during task submission via the ``add_action`` callback
provided by experimaestro:
.. code-block:: python
class Learner(Task):
def __submit__(self, dep, add_action):
loader = dep(self.model.loader_config(...))
# Register export action for the last checkpoint
add_action(self.model.export_action(loader, default_name="last"))
...
:meth:`Module.export_action(loader, **kwargs) `
returns an :class:`~xpm_torch.actions.ExportAction` config by default.
Subclasses override this method to return library-specific actions (e.g.
``XPMIRExportAction`` adds xpmir README sections and metadata).
Customizing the export action
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To customize what happens during export, subclass
:class:`~xpm_torch.actions.ExportAction` and override
:meth:`~xpm_torch.actions.ExportAction.get_hub` to return a
library-specific hub wrapper:
.. code-block:: python
from xpm_torch.actions import ExportAction
class MyExportAction(ExportAction):
def get_hub(self):
return MyCustomHFHub(self.loader)
Then override :meth:`~xpm_torch.module.Module.export_action` on your model
to return this action:
.. code-block:: python
class MyModel(Module):
def export_action(self, loader, **kwargs):
return MyExportAction.C(loader=loader, **kwargs)
Direct API usage
~~~~~~~~~~~~~~~~
You can also use :class:`~xpm_torch.huggingface.TorchHFHub` directly:
.. code-block:: python
from xpm_torch.huggingface import TorchHFHub
# Push a ModuleLoader (from loader_config or validation output)
TorchHFHub(loader).push_to_hub("your-org/model-name")
# Or save locally first
TorchHFHub(loader).save_pretrained("/path/to/save")
:class:`~xpm_torch.huggingface.TorchHFHub` calls
:meth:`~xpm_torch.module.ModuleLoader.write_hub_extras` and
:meth:`~xpm_torch.module.ModuleLoader.hub_readme_sections` on the loader,
so format-specific files (e.g. sentence-transformers configs) and README
sections are generated automatically.
What gets uploaded
~~~~~~~~~~~~~~~~~~
The serialized directory contains:
- ``experimaestro.json`` — the config definition (for reloading with xpmir)
- Model weight directories — named after the loader's ``DataPath`` fields
(or customized via ``__xpm_serialize__``)
- Format-specific configs written by ``write_hub_extras`` (e.g.
``modules.json``, ``router_config.json`` for sentence-transformers)
- ``README.md`` — assembled from base + loader sections
Loading a model from the Hub
-----------------------------
You can load models from the HuggingFace Hub using :class:`~xpm_torch.huggingface.TorchHFHub`.
There are two main ways to load a model depending on whether you want:
- Direct access to the initialized model instance
- Only configuration (loader) itself (for instance in an experiment file).
Loading a model instance
~~~~~~~~~~~~~~~~~~~~~~~~
To load a model as a ready-to-use instance (already initialized and with
weights loaded), use :meth:`~xpm_torch.huggingface.TorchHFHub.from_pretrained`.
This is ideal for direct inference or when you don't need to manipulate
the configuration:
.. code-block:: python
from xpm_torch.huggingface import TorchHFHub
# Returns an initialized Module instance with weights loaded
model = TorchHFHub.from_pretrained("your-org/model-name")
Loading a model loader
~~~~~~~~~~~~~~~~~~~~~~
To load a :class:`~xpm_torch.module.ModuleLoader` configuration instead of
an instance, use :meth:`~xpm_torch.huggingface.TorchHFHub.pretrained_loader`.
This returns a ``ModuleLoader`` config object that can be used as an
initialization task in larger experiments:
.. code-block:: python
from xpm_torch.huggingface import TorchHFHub
# Returns a ModuleLoader config object
loader_cfg = TorchHFHub.pretrained_loader("your-org/model-name")
# The model config is accessible via loader_cfg.model
model_cfg = loader_cfg.model
Low-level access
~~~~~~~~~~~~~~~~
If you need the raw deserialized data from the Hub without any xpm-torch
specific processing, you can use the base experimaestro class:
.. code-block:: python
from experimaestro.huggingface import ExperimaestroHFHub
# Returns the deserialized config (e.g. a ModuleLoader)
data = ExperimaestroHFHub.from_pretrained("your-org/model-name")
Customizing the HF checkpoint format
-------------------------------------
There are three extension points for customizing what gets written during
Hub export:
- :meth:`Module.loader_config(path) ` —
on the model, controls which ``ModuleLoader`` subclass is returned
- :meth:`ModuleLoader.write_hub_extras(save_directory) ` —
on the loader, writes additional files (e.g. ST configs)
- :meth:`ModuleLoader.hub_readme_sections() ` —
on the loader, provides named README sections with positioning
The hooks are on :class:`~xpm_torch.module.ModuleLoader` (not on
:class:`~xpm_torch.module.Module`) because the loader is the object that
gets serialized for Hub export and holds the ``DataPath`` references to
model weights. ``Module`` configs are data-less.
:meth:`~xpm_torch.module.Module.loader_config`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When a model is serialized for the Hub, experimaestro serializes the
:class:`~xpm_torch.module.ModuleLoader` returned by
:meth:`~xpm_torch.module.Module.loader_config`. Subclasses override this
method to return a custom loader with different ``DataPath`` fields:
.. code-block:: python
from xpm_torch.module import Module, SimpleModuleLoader
class MyModel(Module):
def loader_config(self, path):
# Default: single path DataPath
return SimpleModuleLoader.C(value=self, path=path)
Loaders can also override ``__xpm_serialize__`` to control the directory
names used during serialization (e.g. mapping field names to
sentence-transformers conventions as done [here](https://github.com/experimaestro/experimaestro-ir/blob/d685910db9222e7b4b95aaf30e94d6052f27c6f8/src/xpmir/neural/splade.py#L235)).
:meth:`~xpm_torch.module.Module.save_model` / :meth:`~xpm_torch.module.Module.load_model`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
These methods on :class:`~xpm_torch.module.Module` control how model
weights are written to and read from a directory. Override them to change
the on-disk format:
.. code-block:: python
class DualEncoderModel(Module):
encoder: Param[Module]
query_encoder: Param[Optional[Module]]
def save_model(self, path):
path.mkdir(parents=True, exist_ok=True)
self.encoder.save_model(path / "encoder")
if self.query_encoder is not None:
self.query_encoder.save_model(path / "query_encoder")
def load_model(self, path):
self.encoder.load_model(path / "encoder")
if (path / "query_encoder").exists():
self.query_encoder.load_model(path / "query_encoder")
:meth:`~xpm_torch.module.ModuleLoader.write_hub_extras` and :meth:`~xpm_torch.module.ModuleLoader.hub_readme_sections`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To write additional files alongside the model weights during Hub export
(e.g. sentence-transformers compatibility configs), create a custom
:class:`~xpm_torch.module.ModuleLoader` subclass and override
:meth:`~xpm_torch.module.ModuleLoader.write_hub_extras`.
To add sections to the README, override
:meth:`~xpm_torch.module.ModuleLoader.hub_readme_sections` and return a
list of :class:`~xpm_torch.module.ReadmeSection`. Each section has a
``key``, ``content``, and optional ``before``/``after`` constraints for
positioning relative to the base sections (``frontmatter``,
``description``, ``usage``, ``results``):
.. code-block:: python
from xpm_torch.module import ModuleLoader, ReadmeSection
class MyCustomLoader(ModuleLoader):
def write_hub_extras(self, save_directory):
(save_directory / "my_config.json").write_text('{"format": "custom"}')
def hub_readme_sections(self):
return [
ReadmeSection(
key="quick_loading",
content="## Quick loading\n\n```python\nmodel = MyLib.load(...)\n```",
before="usage", # appears before the XPMIR usage section
),
]
These hooks are only called during Hub export (by
:class:`~xpm_torch.huggingface.TorchHFHub`), not during checkpoint saving.
Then override :meth:`~xpm_torch.module.Module.loader_config` on your model
to return this loader:
.. code-block:: python
class MyModel(Module):
def loader_config(self, path):
return MyCustomLoader.C(value=self, path=path)
Class hierarchy
~~~~~~~~~~~~~~~
- :class:`~experimaestro.huggingface.ExperimaestroHFHub` — base serialization
(experimaestro)
- :class:`~xpm_torch.huggingface.TorchHFHub` — calls ``write_hub_extras``
and ``hub_readme_sections`` on the loader (xpm-torch)
- ``XPMIRHFHub`` — adds xpmir README sections (frontmatter, usage, results)
and TensorBoard logs (xpmir)
Utility functions
-----------------
Helper functions and classes for working with HuggingFace Hub:
.. automodule:: xpm_torch.huggingface
:members: