Hosting a SciFM with Nomad¶
This guide walks through the shortest credible path from a scientific model to
a running Nomad tool. The goal is to package the model code, describe it in
nomad.yml, and verify that Nomad can start the server and expose the tool
through MCP.
By the end of the guide, nomad serve ... should start cleanly, your tool
should appear in MCP Inspector, and a test invocation should return output.
What you are building¶
Nomad serves tools from configuration rather than from an ad hoc Python entry point. In practice, connecting a SciFM means supplying four pieces:
Importable Python code for the tool class/function in a pip-installable package.
A reachable model directory for the weights and configuration (
name_or_path),A model card based on container/model-card.md.
A config entry (
fmod_modelsortools) in nomad.yml.
This page focuses on getting that integration working end to end. Specific deployment rules and packaging constraints live in the Deployment Specification.
Create an Importable Package¶
nomad serve loads your class or function by dotted import path, so that exact
symbol must be importable in the same Python environment that launches the
server.
Minimal repository shape:
my_pkg/
pyproject.toml
src/my_pkg/mcp.py
You can create this quickly with
uv init --package ./my_pkg. Replace./my_pkgwith your desired package name.
To quickly check, from the top-level ./my_pkg directory, run:
uv run python -c "from my_pkg.mcp import MyModelTool; print(MyModelTool)"
For local development, install your package into the same environment you use
to run nomad serve. For demo deployments, that environment is defined by
container/deploy/demo/pyproject.toml,
so your package must be added there as a pinned dependency. If the package
comes from another repository or a local checkout, also add a matching
[tool.uv.sources] entry so the demo build can resolve it. Do not
add model-specific dependencies to the root project unless they are required
outside the deployment package.
Writing a TorchModuleTool¶
A nomad.fm_base_tool.TorchModuleTool is the adapter between
Nomad’s MCP-facing tool interface and your underlying PyTorch model. In
practice, you are not exposing your raw training interface. You are defining an
agent-friendly scientific query and then wiring that query into the model.
Before you write code, answer three questions:
What is the scientific query you want the tool to answer?
What should one tool call take as input?
What should one tool call return?
For agent workflows, prefer JSON-friendly inputs and outputs over raw tensors or large binary payloads. A good pattern is to make one tool call represent one scientific query and return one structured answer. That keeps the interface easy for an agent to supply, inspect, and reuse.
For rollout, PDE, or grid-based models, start by looking at
src/nomad/well_format.py. In
particular, nomad.well_format.AutoRegressiveInput and
nomad.well_format.WellFormat provide a ready-made contract for
stateful scientific model inputs and outputs. PDE tools should also document
the model dt in their tool description, prefer t_start=0.0, and ensure
t_final >= duration when using rollout-style inputs.
The four methods you usually implement¶
Most model integrations only need to define:
TorchModuleTool.from_pretrained: load the model and any supporting artifacts such as tokenizers, then return a configured tool instance.TorchModuleTool.preprocess: convert a batch of typed tool inputs into the batched structure your model actually expects.TorchModuleTool._forward: optionally override the default forward path when you need custom model invocation or need to preserve metadata through inference.TorchModuleTool.postprocess: convert batched model outputs back into one typed result per input item.
Minimal skeleton¶
from collections.abc import Sequence
import torch
from pydantic import BaseModel, Field
from nomad.fm_base_tool import TorchModuleTool
# # For PDE Models
# from nomad.well_format import AutoRegressiveInput, WellFormat
# # For SciFM's needing tensor input/outputs
# from nomad.well_format import Tensor
class MyModelInput(BaseModel):
molecule: str = Field(description="Input query for one prediction")
class MyModelOutput(BaseModel):
score: float = Field(description="Predicted value for the input query")
class MyModelTool(
TorchModuleTool[
MyModelInput,
MyModelOutput,
dict[str, torch.Tensor],
torch.Tensor,
]
):
args_schema = MyModelInput
output_schema = MyModelOutput
@classmethod
def from_pretrained(cls, name_or_path: str, **kwargs):
model = load_model_somehow(name_or_path, **kwargs)
return cls(
name="my-model",
description="Predicts a score for one molecule.",
fm=model,
batch_size=32,
device=model.device,
)
def preprocess(self, inputs: Sequence[MyModelInput]) -> dict[str, torch.Tensor]:
encoded = encode_batch([item.molecule for item in inputs])
return {key: value.to(self.device) for key, value in encoded.items()}
def postprocess(self, model_output: torch.Tensor):
for row in model_output:
yield MyModelOutput(score=float(row.item()))
Implementation notes¶
Define
args_schemaandoutput_schemaas Pydantic models that describe the MCP tool interface, not your raw model tensors.Put heavyweight loading logic in
TorchModuleTool.from_pretrained, not__init__.In
TorchModuleTool.preprocess, assume you are receiving a batch. Even if each call looks scalar at the CLI or in MCP Inspector, Nomad may batch several requests together.Move tensors to
self.deviceinTorchModuleTool.preprocess.TorchModuleToolexecution should use only that device. Multi-GPU models are not supported.In
TorchModuleTool.postprocess, yield one output object at a time rather than returning the whole batch as one result.Use the tool description and field descriptions carefully. Agents and human operators both rely on that text to decide when and how to call the tool. Keep descriptions concise and put detailed model information in the model card.
If your model produces very large outputs, rethink the tool interface before exposing it directly. Large payloads are awkward for both MCP clients and language-model-driven agents.
Do not write to stdout from model or tool code. Use Python logging instead.
Tools should not write persistent files. If a temporary file is unavoidable, create it per invocation with the
tempfilemodule.
Overriding
__init__is highly discouraged becauseTorchModuleToolis apydantic.BaseModel. Custom__init__behavior is possible, but considerably more complex than loading the model inTorchModuleTool.from_pretrained.
Choose fmod_models vs tools¶
Use
fmod_modelswhen your class is anomad.fm_base_tool.TorchModuleTool.Use
toolsfor regular callables, such as CPU-only Python functions.
If you are serving one SciFM, start with a complete nomad.yml like this:
fmod_models:
- model_class: my_pkg.mcp.MyModelTool
name_or_path: my-org/my-model-v2
tool_name: my-model-v2
batch_size: 32
Add entries under tools only when you also want regular Python callables
registered alongside the model.
Hosting Your Model Weights¶
The weights and configuration for your model must be stored in a
HuggingFace-style model directory.
Prefer hosting that directory as an external model source. The path to this
directory is passed to
TorchModuleTool.from_pretrained
to load your model. This directory is referenced by the name_or_path field
for the model under fmod_models in the config.
TorchModuleTool.from_pretrained(name_or_path) should accept the resolved
name_or_path without requiring extra keyword arguments and should return a
fresh, independent tool instance each time it is called. The tool manager may
call it repeatedly while serving a model; see Tool Manager Reference
for lifecycle details.
Recommended name_or_path values:
Source |
Example |
|---|---|
|
|
|
|
|
You may use a local path for
name_or_pathduring testing, but this is not supported for demo deployments. ORAS artifacts are preferred.
If you use a local model directory for development, choose a directory name that satisfies MCP tool naming constraints, use a distinct directory name for each weight revision, and include a model card matching the template.
Upload Weights To HuggingFace¶
Use HuggingFace when you want to publish the model directory as a Hub model repository. Follow HuggingFace’s uploading models guide to create the repository and upload the files.
Once the model is uploaded, reference it with an hf:// URI:
name_or_path: hf://my-org/my-model-v2
If the model files are inside a subdirectory of the repository, add that subdirectory as a fragment:
name_or_path: hf://my-org/my-model-v2#models/my-model-v2
Push Weights To ORAS¶
Use ORAS when you want model weights in an OCI-compatible registry, separate from your source repository. Start with a HuggingFace-style model directory:
models/my-model-v1/
config.json
model.safetensors
tokenizer.json
Install ORAS, log in to your registry, then push the model directory from the project root:
oras login registry.example.org
oras push \
registry.example.org/my-org/my-model:v1 \
models/my-model-v1/
Reference the pushed artifact in nomad.yml with the model directory as a
subpath:
name_or_path: oras://registry.example.org/my-org/my-model:v1#models/my-model-v1
For reproducible deployments, resolve the tag to a digest and use the pinned URI:
oras resolve registry.example.org/my-org/my-model:v1
name_or_path: oras://registry.example.org/my-org/my-model@sha256:...#models/my-model-v1
If the registry is private, make sure the environment that runs Nomad can authenticate to it before startup.
Store Weights With Git LFS¶
Weights hosted within your project’s repository MUST be stored with Git LFS. First, install Git LFS. The model directory should still match the HuggingFace model repository format. Then, within your project’s repository:
git lfs install
git lfs track '*.safetensors' '*.pth' # Plus any other large files used by your model
git add .gitattributes
git add path/to/model_directory
git commit -m "Adding model"
git push
Validate¶
Run:
nomad serve --transport streamable-http --host localhost --port 8000 path/to/nomad.yml
Expected output (truncated):
INFO: Application startup complete.
INFO: Uvicorn running on http://localhost:8000
If your tool accepts ordinary JSON-friendly inputs, connect with MCP Inspector and run a quick tool call to verify that it behaves correctly.
Structured Tensor Inputs¶
If your TorchModuleTool accepts
WellFormat,
AutoRegressiveInput, or
nomad.well_format.Tensor fields, MCP Inspector is usually the
wrong testing surface. Those values are sent as compressed base64-encoded torch
serializations, which makes hand-written Inspector payloads cumbersome and
error-prone.
In that case, test the model through nomad code-mode-exec
instead.
Create gateway.yml:
servers:
nomad:
transport: streamable_http
url: http://localhost:8000/mcp
Then create test_rollout.py. The generated Python wrapper name is the
sanitized form of your MCP tool name, so my-model-v2 becomes my_model_v2:
from mcp_tools.nomad import my_model_v2
from nomad.well_format import WellFormat
initial_state = WellFormat.from_file("path/to/input.h5")
# Nomad decodes tensor fields returned by wrappers, but does not
# reconstruct the outer Pydantic object automatically.
rollout = my_model_v2(
duration=10,
initial_state=initial_state,
)
# Reconstruct WellFormat from the returned plain-dict output
rollout = WellFormat.model_validate(rollout)
rollout.to_file("path/to/output.h5")
Run the script in the code-mode sandbox:
nomad code-mode-exec --config gateway.yml test_rollout.py
For a fuller worked example using WellFormat inputs and a PDE surrogate, see
the Nomad Inference notebook.
Demo Deployment Additions¶
If you are wiring a new model into the repository’s demo deployment scaffold, update these files together:
container/deploy/demo/pyproject.toml: add your model package to
dependencies, and add or update the matching[tool.uv.sources]entry if the package comes from Git or a local checkout.container/deploy/demo/nomad.yml: add your
fmod_modelsentry, plus any helpertoolsentries andtool_managersettings needed for batching or queue depth.container/model-card.md: use this template when you are shipping a local model directory that needs a model card in the exported bundle.
Paths in
container/deploy/demo/nomad.yml
are resolved relative to that file. During image builds,
the demo image build process
runs nomad export before building the container, so local model assets
referenced by name_or_path are copied into the build context automatically.
For the full demo deployment workflow, see the Deployment Guide.