Skip to content

Building a New Server

The creation of a Melissa server is largely based on inheritance, which establishes the structure for study execution. Depending on the type of study, users must inherit a specific server class defined in Melissa and extend it with their own functionalities.

Note

The majority of the melissa.server module is statically typed, which can be helpful for identifying attributes and methods from super classes. This is especially useful if users are using IDEs with support for Pylance or similar extensions.

Melissa server class hierarchy

hierarchy

Based on the hierarchy shown above, users are expected to inherit one of the child classes.

To begin, create a new Python script that defines the custom server class. Both the script and the class name are specified in the server_filename and server_class options in the configuration file.

Tip

Due to the deeper inheritance structure, understanding the available attributes and methods can be challenging. We recommend reviewing the server documentation to familiarize yourself with all the attributes exposed in the user server class.

Deep-Learning Server

Melissa's DeepMelissaServer class key aspects:

Following code snippet showcases an example provided in examples/heat-pde/heatpde_dl_server.py script. Users must refer to this and then build their own use-case specific servers.

import torch
from melissa.server.deep_learning.torch_server import TorchServer
from melissa.server.parameters import ParameterSamplerType

from local_module import MyModel

logger = logging.getLogger("melissa")


class HeatPDEServerDL(TorchServer):
    """Use-case specific server"""

    def __init__(self, config_dict: dict[str, Any]):
        super().__init__(config_dict)
        self.param_list = ["ic", "b1", "b2", "b3", "b4", "t"]
        study_options = self.config_dict["study_options"]

        # custom options
        self.mesh_size = study_options["mesh_size"]
        Tmin, Tmax = study_options["parameter_range"]

        # example of random uniform sampling
        self.set_parameter_sampler(
            sampler_t=ParameterSamplerType.RANDOM_UNIFORM, l_bounds=[Tmin], u_bounds=[Tmax]
        )

        # setting the attribute valid_dataloader allows validation
        # to be run alongside training
        self.valid_dataloader = self.get_validation_dataloader()  # user-defined

    @override
    def prepare_training_attributes(self):
        """Abstract method that must return model and optimizer."""

        model = self.wrap_model_ddp(
            self.MyModel(self.nb_parameters + 1, self.mesh_size * self.mesh_size, 1).to(self.device)
        )

        optimizer = torch.optim.Adam(
            model.parameters(), lr=self.dl_config.get("lr", 1e-3), weight_decay=1e-4
        )

        return model, optimizer

    @override
    def training_step(self, batch, batch_idx, **kwargs):

        # backprogation
        self.optimizer.zero_grad()
        x, y_target = batch
        x = x.to(self.device)
        y_target = y_target.to(self.device)
        y_pred = self.model(x)
        loss = self.criterion(y_pred, y_target)
        loss.backward()
        self.optimizer.step()
        self.learning_rate_scheduler.step()
        self.metric_logger.log_scalar("Loss/train", loss.item(), batch_idx)
        logger.info(f"[TRAINING] batch-id={batch_id} loss={loss.item():.2e}")

    @override
    def process_simulation_data(self, msg: SimulationData, config_dict: dict):
        """Abstract method for transformation while batch creation."""

        field = "temperature"
        # cast msg.data to float32
        x = torch.from_numpy(
            np.array(msg.parameters[-self.nb_parameters :] + [msg.time_step], dtype=np.float32)
        )
        y = torch.from_numpy(msg[field].astype(np.float32))

        return x, y
Processing if a conduit Node was sent
@override
def process_simulation_data(self, msg: SimulationData, config_dict: dict):
    """Abstract method for transformation while batch creation."""

    field = "temperature"

    # cast msg.data to float32
    x = torch.from_numpy(
        np.array(msg.parameters[-self.nb_parameters :] + [msg.time_step], dtype=np.float32)
    )
    temp_node: conduit.Node = msg[field]
    data: np.ndarray = temp_node["mesh/data"]
    current_sim_time: float = temp_node["metadata/sim_time"]
    dt: float = temp_node["metadata/dt"]

    y = torch.from_numpy(data.astype(np.float32))

    return x, y

At this point, users should look at the whole conduit node workflow to concretize their understanding.

Reception is Grouped by the Simulation and its Timesteps

For every melissa::send called per node_name, Melissa server groups values by (simulation_id, time_step) such that the buffer maintains values altogether.

If a solver sends the following nodes in the same timestep like:

melissa::send("temperature", temp_node);
melissa::send("pressure", pressure_node);
melissa::send("velocity", velocity_vector);

Then the user-defined server can extract msg instance grouped by (msg.simulation_id, msg.time_step) like:

@override
def process_simulation_data(self, msg: SimulationData, config_dict: dict):
    """Abstract method for transformation while batch creation."""
    assert isinstance(msg, SimulationData)
    temp_node: conduit.Node = msg["temperature"]
    pressure_node: conduit.Node = msg["pressure"]
    velocity_arr: np.array = msg["velocity"]

Modifications for training

  • Users can access JSON configuration options through config_dict dictionary. Although, some are already exposed as attributes in Melissa server super classes such as nb_simulations, nb_time_steps, etc. (See exposed properties section below)

  • Call self.set_parameter_sampler, which accepts either the pre-defined Enum values (ParameterSamplerType) or a custom sampler class type that inherits base classes.

  • Assuming your model architecture is already implemented, override prepare_training_attributes to return the (model, optimizer) tuple.

Tip

When working on DataDistributedParallel with PyTorch, users must call self.wrap_model_ddp on the model instance to convert it into a DDP-compatible model. With different frameworks, users must define their DDP-style model updations in this function.

  • Override process_simulation_data, a transformation method applied to data retrieved from the buffer when creating a batch. This method takes an instance of SimulationData and the config_dict containing all configuration settings from the JSON file.

  • Override training_step, which accepts the transformed data (batch) and the current batch index (batch_idx) as inputs.

Note

  • The SimulationData object contains a flattened numpy arrays. Ensure the shape is correct before passing it to the model. For variable shapes, users can send the shape list along with their data in a Conduit Node.

  • The choice of an iterable dataloader for online training depends on the framework being used. If TorchServer is inherited, Melissa will instantiate a pytorch dataloader. However, the default GeneralDataLoader works for framework-agnostic training loops.

Modifications for Validation (Optional)

To enable validation, users need to follow these steps:

  • The DeepMelissaServer training loop expects self.valid_dataloader to be set, but its definition is left to the user. The provided code snippet initializes self.valid_dataloader through a custom method.

  • Override the validation_step method, which takes 3 inputs:

  • the validation data (batch) from self.valid_dataloader
  • the validation batch index (valid_batch_idx)
  • the training batch index (batch_idx)

Note

By default, validation loop executes every nb_batch_updatesth training batch.

More Control (Optional)

For users who need greater flexibility over the training loop, DeepMelissaServer provides several hook methods (pytorch-lightning like) that are triggered at specific points during training:

Method Description
on_train_start() Called at the start of training.
on_train_end() Called at the end of training.
on_batch_start(batch_idx) Called at the start of a batch iteration.
on_batch_end(batch_idx) Called at the end of a batch iteration.
on_validation_start(batch_idx) Called at the start of validation.
on_validation_end(batch_idx) Called at the end of validation.

Tip

Advanced users familiar with the DeepMelissaServer class can override the train and validation methods of the super class directly.

Exposed properties

Following are some of the important server attributes users may requires:

Property Description
metric_logger Provides access to a metric logger instance. (defaults to using Tensorboard)
buffer Returns the buffer instance.
optimizer Gets or sets the optimizer. Must be set using prepare_training_attributes.
model Gets or sets the model. Must be set using prepare_training_attributes.
dataset Gets or sets the dataset created from the buffer.
valid_dataloader Gets or sets the validation dataloader. Must be set by the user.