Skip to content

Add hooks for indirect learning. - #1209

Open
pfebrer wants to merge 15 commits into
metatensor:mainfrom
pfebrer:post_hooks
Open

Add hooks for indirect learning.#1209
pfebrer wants to merge 15 commits into
metatensor:mainfrom
pfebrer:post_hooks

Conversation

@pfebrer

@pfebrer pfebrer commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

After discussion with @ppegolo we agreed that the cleanest way to implement indirect learning in metatrain was through hooks that run at the end of the model.

Why a hook solves things: If you ask for a target to be produced by a hook, the hook will (1) remove the target from the outputs that the model should produce and (2) add the hook's inputs as extra outputs that the model has to produce.

Proof of concept: I implemented a hook to compute global multipoles from local predictions, since this is one of the use cases that needed the functionality. The idea is that architectures fully control these things, in the spirit of metatrain. You can see the proof of concept implementation in MACE. I would like to first get some feedback before continuing. Hopefully at this point it is simple enough to understand it easily.

The input yaml: Here is an input yaml to learn dipoles through the global_multipoles hook.

architecture:
  name: experimental.mace
  model:
    hidden_irreps: 10x0e + 10x1o + 10x2e
    post_hooks:
      global_multipoles: mtt::dipole

training_set:
  systems: QM7X_train_1024.zip
  targets:
    mtt::dipole:
      type:
        spherical:
          irreps: [{o3_lambda: 1, o3_sigma: 1}]

validation_set: 0.1
test_set: 0.0

Here is the dataset with dipoles: QM7X_train_1024.zip

Of course one can train direct targets at the same time as the indirect dipole.


📚 Documentation preview 📚: https://metatrain--1209.org.readthedocs.build/en/1209/

@pfebrer

pfebrer commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

Seems to work :)
image

@Luthaf Luthaf left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the hook idea makes sense and can be very useful!

One question though: is it a hook job to take per atom prediction and make it per-system? It kinda look like this is the case here? Or I might have misunderstood the example


# Build the input target that we will request from the model,
# which is the local multipoles
self._input_name = "mtt::aux::local_multipoles"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be useful to have some documentation about the custom metatrain outputs somewhere, especially if we start using them across multiple architectures with theses hooks. (Even better, standardizing what makes sense at the metatomic level!)

@pfebrer

pfebrer commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Thanks!

One question though: is it a hook job to take per atom prediction and make it per-system? It kinda look like this is the case here? Or I might have misunderstood the example

There could be a hook to do that, specially for cases when the aggregation is not simply a sum, but something more complicated like softmin/max for the bandgaps. I guess the case of the global dipoles can be seen as that, we are doing dipole = sum(local_charge * positions + local_dipole), so it's not just a simple sum.

@pfebrer

pfebrer commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

I guess indeed with standardization of outputs these hooks might become applied by default when needed, instead of the user having to specify them.

@pfebrer

pfebrer commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@cesaremalosso I have added the hook to produce gap-like targets. I have been testing it and seems to work with both PET (thanks to Joe) and MACE, give it a test when you can :) I basically copied the functionality that you had in your branch, and now there's a hook called minmax_gap. Don't worry the name is not definitive haha

The way of using it is:

architecture:
  name: pet | experimental.mace
  model:
    ...other model hypers
    post_hooks:
      minmax_gap: mtt::gap_energy
  training:
    atomic_baseline: {mtt::gap_energy: 0.0}
    ...whatever

training_set:
  systems: train_structures.xyz
  targets:
    mtt::gap_energy:
      quantity: energy
      key: Gap_energy
      sample_kind: system
      type: scalar
      forces:
        key: Gap_forces

also you can tune the hook further with something like:

post_hooks:
  minmax_gap:
    outputs: mtt::gap_energy
    pooling: {type: softmax}

Let me know if it works fine!

@pfebrer

pfebrer commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Hmm for the gap case the composition model needs to be disabled, I wonder if this should be the job of the hook to warn about this. It certainly complicates things 😅

@pfebrer

pfebrer commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Also Cesare is asking if one could do finetuning of an MLIP by adding these hooks, and I see no easy way, since right now the hooks are specified as model hypers, which are not used when finetuning I think (?)

So maybe the right design of a UI would be that users specify these hooks in the target itself:

targets:
  mtt::gap_energy:
    ...target specification
    quantity: intensive_gap # Or whatever way of specifying it.

this would make it cleaner for the composition model to avoid this target and for the finetuning to detect it, and probably also be in the spirit of metatrain of "we should require as litle knowledge from users".

But then some cases become more complicated, like when a hook has hyperparameters or when a hook produces multiple outputs (e.g. a diagonalization hook that produces eigenvalues, eigenvectors, density matrices, and other electronic structure properties). In general, passing the category in the targets results in a less flexible UI I think, for the good and for the bad 😅

@ceriottm

Copy link
Copy Markdown
Contributor

Geeez, this looks very complicated, but also very useful. So the idea is that you can specify generic modules to be applied after a head? I think that defining them in the targets as post_hooks could be a good solution no? or does that interfere with mtt understanding what the actual outputs are?

@pfebrer

pfebrer commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

In essence it is what you are saying, and very similar to the approach of metatomic to define wrappers, except these hooks work also during training. Now the problem is that I don't know what is the best way to expose it to the users.

In the way metatrain does things, I believe it has to be fully controlled by the architecture. So specifying a full hook in the target seems a bit strange, as some architectures might not want to use that hook (for example a graph2mat hook for edges, PET will not want to use it). I think it is more metatrain-like to do:

  • Specify quantity in the target.
  • Architecture decides that for this quantity it is going to offload the work to hooks.

But then you lose quite some flexibility (do we want that flexibility?). Specifying the full hook in the target would indeed require modifying metatrain.TargetInfo and metatomic.ModelOutput, so it seems it would be a more involved path. But maybe it is the way to go, I don't know. This is why I'm trying to gather examples that would use the functionality, to see if there's a simple way to make them all work.

@pfebrer

pfebrer commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Ok so with this we have the three examples that I had in mind for hooks:

  • Softmin/max for gaps (custom pooling from atomic contributions).
  • Global multipoles (pooling from atomic contributions involving the systems' positions).
  • Tensor basis (hook with learnable parameters).

The other two that I had involve matrices (graph2mat hook and eigenvalues hook) so they can't be added yet. If someone has more use cases let me know and let's add them. Then once we have all the examples I'll start refining the implementation.

@jwa7

jwa7 commented Jul 24, 2026

Copy link
Copy Markdown
Member

I think the matrix use case is sufficiently complex and useful to consider here in the design (even if not directly implementable yet from the current state of main) as it is different from the others: for the Hamiltonian, for example, we might want to directly and indirectly learn on it simultaneously (i.e. H in the loss function as well as derived eigs and DM), and the basis set still needs passing somehow to the model. This is different from the total dipole, for instance, where the dimensions of the indirectly learned charges and vector contributions are predetermined, there is no reference data for them, and they only enter the loss as a derived quantity (i.e. no term explicitly on them)

@pfebrer

pfebrer commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Yes, the basis information will be passed through dataset_info, and it will be possible to use targets as inputs for the hooks

@jwa7

jwa7 commented Jul 24, 2026

Copy link
Copy Markdown
Member

the basis information will be passed through dataset_info

Ok good good.

A small side note to be aware of: we need to make sure that the hooks are compatible with the optional dependencies installed by the user. Currently when I install with just the base dependencies (i.e. for PET) I get a spex import error from the tensor basis hook module.

@pfebrer

pfebrer commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Yeah I thought about this, probably optional dependencies for hooks should be in pyproject.toml as [hook-*]

pfebrer and others added 7 commits July 25, 2026 22:38
* fix output block  with cuda values but cpu labels

* Move layout's device once at the top

---------

Co-authored-by: Pol Febrer Calabozo <42074085+pfebrer@users.noreply.github.com>
@pfebrer

pfebrer commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

Ok so this got x10 more complex to review haha it's probably best to explain in person.

Essentially we realised that at some point we would need checkpoint updates, things need to be properly documented, well tested, etc... so I decided to follow the same design as with architectures (folder for each hook, automatic documentation, common tests...).

Still not finished, but decided to push so that people can modify the hooks in this new setup.

I'm happy because the hooks seem like a very nice way to release code when people publish papers, which has been a bit of a problem lately since modifying an architecture is not easy. The barrier for making it serious enough for a hook is lower and there is no friction with architecture maintainers.

pfebrer and others added 7 commits July 26, 2026 14:12
This is done by storing the hooks in DatasetInfo instead of in the
model hypers. To do this, hooks config is moved to a top level "hooks"
in the yaml file.

There are still things to do, like supporting finetuning a model that
already has hooks, but with this people can already do first tests.
Removed post_hooks parameter from TrainerHypers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants