https://mooseframework.inl.gov
NEML2ModelExecutor.C
Go to the documentation of this file.
1 //* This file is part of the MOOSE framework
2 //* https://mooseframework.inl.gov
3 //*
4 //* All rights reserved, see COPYRIGHT for full restrictions
5 //* https://github.com/idaholab/moose/blob/master/COPYRIGHT
6 //*
7 //* Licensed under LGPL 2.1, please see LICENSE for details
8 //* https://www.gnu.org/licenses/lgpl-2.1.html
9 
10 #include "NEML2ModelExecutor.h"
11 #include "MOOSEToNEML2.h"
12 #include "NEML2Utils.h"
13 #include <string>
14 #include <sstream>
15 
16 #ifdef NEML2_ENABLED
17 #include <ATen/ATen.h>
18 #include "libmesh/id_types.h"
19 #include "neml2/tensors/functions/jacrev.h"
20 #include "neml2/dispatchers/ValueMapLoader.h"
21 #include "neml2/misc/string_utils.h"
22 #include "neml2/base/Settings.h"
23 #endif
24 
26 
29 {
30  auto params = emptyInputParameters();
31  params.addParam<bool>(
32  "manage_state_advance",
33  false,
34  "Keep state and forces on the device and advance it to old_state and old_forces without a "
35  "roundtrip through MOOSE materials. This is only recommended for explicit time integration "
36  "or when absolutely no restepping occurs (e.g. failed timesteps).");
37  params.addParam<bool>(
38  "debug_inputs_on_failure",
39  false,
40  "When a NEML2 solve fails, append a detailed dump of input tensors (defined/missing, "
41  "shapes, and devices) to the error message.");
42  return params;
43 }
44 
47 {
50  params.addClassDescription("Execute the specified NEML2 model");
51 
52  params.addRequiredParam<UserObjectName>(
53  "batch_index_generator",
54  "The NEML2BatchIndexGenerator used to generate the element-to-batch-index map.");
55  params.addParam<std::vector<UserObjectName>>(
56  "gatherers",
57  {},
58  "List of MOOSE*ToNEML2 user objects gathering MOOSE data as NEML2 input variables");
59  params.addParam<std::vector<UserObjectName>>(
60  "param_gatherers",
61  {},
62  "List of MOOSE*ToNEML2 user objects gathering MOOSE data as NEML2 model parameters");
63 
64  // Since we use the NEML2 model to evaluate the residual AND the Jacobian at the same time, we
65  // want to execute this user object only at execute_on = LINEAR (i.e. during residual evaluation).
66  // The NONLINEAR exec flag below is for computing Jacobian during automatic scaling.
69  params.set<ExecFlagEnum>("execute_on") = execute_options;
70 
71  return params;
72 }
73 
76 #ifdef NEML2_ENABLED
77  ,
78  _batch_index_generator(getUserObject<NEML2BatchIndexGenerator>("batch_index_generator")),
79  _manage_state_advance(getParam<bool>("manage_state_advance")),
80  _debug_inputs_on_failure(getParam<bool>("debug_inputs_on_failure")),
81  _output_ready(false),
82  _error_message("")
83 #endif
84 {
85 #ifdef NEML2_ENABLED
86  validateModel();
87 
88  // add user object dependencies by name (the UOs do not need to exist yet for this)
89  for (const auto & gatherer_name : getParam<std::vector<UserObjectName>>("gatherers"))
90  _depend_uo.insert(gatherer_name);
91  for (const auto & gatherer_name : getParam<std::vector<UserObjectName>>("param_gatherers"))
92  _depend_uo.insert(gatherer_name);
93 #endif
94 }
95 
96 #ifdef NEML2_ENABLED
97 void
99 {
100  // deal with user object provided inputs
101  for (const auto & gatherer_name : getParam<std::vector<UserObjectName>>("gatherers"))
102  {
103  // gather coupled user objects late to ensure they are constructed. Do not add them as
104  // dependencies (that's already done in the constructor).
105  const auto & uo = getUserObjectByName<MOOSEToNEML2>(gatherer_name, /*is_dependency=*/false);
106 
107  // there's no need to gather old/older variables if we're managing state advance
108  auto sep = model().settings().history_separator();
109  auto [base_name, history_order] = neml2::parse_history(uo.NEML2Name(), sep);
110  if (_manage_state_advance && history_order > 0)
111  paramError("gatherers",
112  "The gatherer for history variable `",
113  uo.NEML2Name(),
114  "` is not needed when `manage_state_advance = true`.");
115 
116  addGatheredVariable(gatherer_name, uo.NEML2Name());
117  _gatherers.push_back(&uo);
118  }
119 
120  // deal with user object provided model parameters
121  for (const auto & gatherer_name : getParam<std::vector<UserObjectName>>("param_gatherers"))
122  {
123  // gather coupled user objects late to ensure they are constructed. Do not add them as
124  // dependencies (that's already done in the constructor).
125  const auto & uo = getUserObjectByName<MOOSEToNEML2>(gatherer_name, /*is_dependency=*/false);
126  addGatheredParameter(gatherer_name, uo.NEML2Name());
127  _param_gatherers.push_back(&uo);
128  }
129 
130  // iterate over set of required inputs and error out if we find one that is not provided
131  for (const auto & [iname, ivar] : model().input_variables())
132  {
133  // if tensors are kept on device, we are not going to gather old values from moose
134  if (_manage_state_advance && ivar->history_order() > 0)
135  continue;
136  if (!_gathered_variable_names.count(iname))
137  paramError("gatherers", "The required model input `", iname, "` is not gathered");
138  }
139 
140  // keep track of stateful variables if manage_state_advance is true
142  for (const auto & [iname, ivar] : model().input_variables())
143  if (ivar->history_order() > 0)
144  _state_vars[iname] = neml2::Tensor();
145 }
146 
147 std::size_t
149 {
150  return _batch_index_generator.getBatchIndex(elem_id);
151 }
152 
153 void
154 NEML2ModelExecutor::addGatheredVariable(const UserObjectName & gatherer_name,
155  const neml2::VariableName & var)
156 {
157  if (_gathered_variable_names.count(var))
158  paramError("gatherers",
159  "The NEML2 input variable `",
160  var,
161  "` gathered by UO '",
162  gatherer_name,
163  "' is already gathered by another gatherer.");
164  _gathered_variable_names.insert(var);
165 }
166 
167 void
168 NEML2ModelExecutor::addGatheredParameter(const UserObjectName & gatherer_name,
169  const std::string & param)
170 {
171  if (_gathered_parameter_names.count(param))
172  paramError("gatherers",
173  "The NEML2 model parameter `",
174  param,
175  "` gathered by UO '",
176  gatherer_name,
177  "' is already gathered by another gatherer.");
178  _gathered_parameter_names.insert(param);
179 }
180 
181 void
183 {
185  return;
186 
187  _output_ready = false;
188  _error = false;
189  _error_message.clear();
190 }
191 
192 void
194 {
196  return;
197 
198  _output_ready = false;
200  mooseError("The mesh changed while `manage_state_advance = true` for NEML2 model executor '",
201  name(),
202  "'. This mode requires a fixed mesh because state history is cached on the device.");
203 }
204 
205 void
207 {
209  return;
210 
212  {
214  advanceState();
215  return;
216  }
217 
218  // If the batch is empty, we do not need to do anything
220  return;
221 
222  fillInputs();
223 
224  if (_t_step > 0)
225  {
226  auto success = solve();
227  if (success)
228  extractOutputs();
229  }
230 }
231 
232 void
234 {
235  try
236  {
237  for (const auto & uo : _gatherers)
238  uo->insertInto(_in);
239  for (const auto & uo : _param_gatherers)
240  uo->insertInto(_model_params);
241 
242  if (_manage_state_advance && _t_step > 0)
243  for (const auto & [name, val] : _state_vars)
244  if (val.defined())
245  _in[name] = val;
246 
247  // Send input variables and parameters to device
248  for (auto & [var, val] : _in)
249  val = val.to(device());
250  for (auto & [param, pval] : _model_params)
251  pval = pval.to(device());
252 
253  // Update model parameters
254  model().set_parameters(_model_params);
255  _model_params.clear();
256 
257  // Request gradient for the model parameters that we request AD for
258  for (const auto & [y, dy] : _retrieved_parameter_derivatives)
259  for (const auto & [p, tensor] : dy)
260  model().get_parameter(p).requires_grad_(true);
261  }
262  catch (std::exception & e)
263  {
264  mooseError("An error occurred while filling inputs for the NEML2 model. Error message:\n",
265  e.what(),
267  }
268 }
269 
270 void
272 {
273  // Figure out what our batch size is
274  std::vector<neml2::Tensor> defined;
275  for (const auto & [key, value] : _in)
276  defined.push_back(value);
277  const auto s = neml2::utils::broadcast_dynamic_sizes(defined);
278 
279  // Make all inputs conformal
280  for (auto & [key, value] : _in)
281  if (value.dynamic_sizes() != s)
282  _in[key] = value.dynamic_unsqueeze(0).dynamic_expand(s);
283 }
284 
285 void
287 {
288  if (!_manage_state_advance || _t_step == 0)
289  return;
290 
291  for (const auto & [name, val] : _state_vars)
292  {
293  auto sep = model().settings().history_separator();
294  auto [base_name, order] = neml2::parse_history(name, sep);
295  mooseAssert(order > 0, "Invalid history order");
296  // cache value from the current step
297  // favor output over input
298  auto curr_name = order == 1 ? base_name : base_name + sep + std::to_string(order - 1);
299  if (_out.count(curr_name))
300  _state_vars[name] = _out.at(curr_name);
301  else if (_in.count(curr_name))
302  _state_vars[name] = _in.at(curr_name);
303  else
304  mooseError("Failed to find cached value for history variable: ", name);
305  }
306 }
307 
308 bool
310 {
311  try
312  {
313  // Evaluate the NEML2 material model
314  TIME_SECTION("NEML2 solve", 3, "Solving NEML2 material model");
315 
316  // NEML2 requires double precision
317  auto prev_dtype = neml2::get_default_dtype();
318  neml2::set_default_dtype(neml2::kFloat64);
319 
320  if (scheduler())
321  {
322  // We only need consistent batch sizes if we are using the dispatcher
323  expandInputs();
324  neml2::ValueMapLoader loader(_in, 0);
325  std::tie(_out, _dout_din) = dispatcher()->run(loader);
326  }
327  else
328  std::tie(_out, _dout_din) = model().value_and_dvalue(_in);
330  _in.clear();
331 
332  // Restore the default dtype
333  neml2::set_default_dtype(prev_dtype);
334  }
335  catch (std::exception & e)
336  {
337  _error_message = e.what();
338  _error = true;
340  {
341  auto shape_to_string = [](const neml2::TensorShapeRef & shape) -> std::string
342  {
343  std::ostringstream os;
344  os << "(";
345  for (std::size_t i = 0; i < shape.size(); ++i)
346  {
347  if (i)
348  os << ", ";
349  os << shape[i];
350  }
351  os << ")";
352  return os.str();
353  };
354 
355  std::ostringstream os;
356  os << "\nNEML2 input variables:\n";
357  for (const auto & [var, val] : model().input_variables())
358  {
359  os << " - " << var << ": ";
360  const auto it = _in.find(var);
361  if (it == _in.end())
362  os << "missing\n";
363  else if (!it->second.defined())
364  os << "undefined\n";
365  else
366  {
367  const auto & val = it->second;
368  const auto & v = model().input_variable(var);
369  neml2::TensorShape expected;
370  const auto & intmd_sizes = v.intmd_sizes();
371  expected.insert(expected.end(), intmd_sizes.begin(), intmd_sizes.end());
372  const auto & base_sizes = v.base_sizes();
373  expected.insert(expected.end(), base_sizes.begin(), base_sizes.end());
374 
375  os << "device=" << val.device() << " dtype=" << val.scalar_type()
376  << " sizes=" << shape_to_string(val.sizes())
377  << " batch=" << shape_to_string(val.batch_sizes().concrete())
378  << " expected_base=" << shape_to_string(expected);
379 
380  if (val.numel() > 0)
381  {
382  auto cpu = val.detach().to(val.options().device(at::kCPU));
383  auto flat = cpu.reshape({-1});
384  auto min = flat.min().item<double>();
385  auto max = flat.max().item<double>();
386  auto mean = flat.mean().item<double>();
387  auto has_nan = at::isnan(flat).any().item<bool>();
388  auto has_inf = at::isinf(flat).any().item<bool>();
389  os << " min=" << min << " max=" << max << " mean=" << mean
390  << " nan=" << (has_nan ? "true" : "false")
391  << " inf=" << (has_inf ? "true" : "false");
392  }
393 
394  os << "\n";
395  }
396  }
397 
399  {
400  os << "NEML2 stateful variables:\n";
401  for (const auto & [var, cached_val] : _state_vars)
402  {
403  os << " - " << var << ": ";
404  const auto it_out = _out.find(var);
405  const auto it_in = _in.find(var);
406  if (it_out == _out.end() || it_in == _in.end())
407  os << "missing\n";
408  else
409  {
410  const auto it = it_out != _out.end() ? it_out : it_in;
411  const auto & val = it->second;
412  os << "device=" << val.device() << " dtype=" << val.scalar_type()
413  << " sizes=" << shape_to_string(val.sizes())
414  << " batch=" << shape_to_string(val.batch_sizes().concrete());
415 
416  if (val.numel() > 0)
417  {
418  auto cpu = val.detach().to(val.options().device(at::kCPU));
419  auto flat = cpu.reshape({-1});
420  auto min = flat.min().item<double>();
421  auto max = flat.max().item<double>();
422  auto mean = flat.mean().item<double>();
423  auto has_nan = at::isnan(flat).any().item<bool>();
424  auto has_inf = at::isinf(flat).any().item<bool>();
425  os << " min=" << min << " max=" << max << " mean=" << mean
426  << " nan=" << (has_nan ? "true" : "false")
427  << " inf=" << (has_inf ? "true" : "false");
428  }
429 
430  os << "\n";
431  }
432  }
433  }
434  _error_message += os.str();
435  }
436  }
437 
438  return !_error;
439 }
440 
441 void
443 {
444  try
445  {
446  const auto N = _batch_index_generator.getBatchIndex();
447 
448  // retrieve outputs
449  for (auto & [y, target] : _retrieved_outputs)
450  target = _out[y].to(output_device());
451 
452  // retrieve parameter derivatives
453  for (auto & [y, dy] : _retrieved_parameter_derivatives)
454  for (auto & [p, target] : dy)
455  target = neml2::jacrev(_out[y],
456  model().get_parameter(p),
457  /*retain_graph=*/true,
458  /*create_graph=*/false,
459  /*allow_unused=*/false)
460  .to(output_device());
461 
462  // clear output unless we need it for on-device state advance
464  _out.clear();
465 
466  // retrieve derivatives
467  for (auto & [y, dy] : _retrieved_derivatives)
468  for (auto & [x, target] : dy)
469  {
470  const auto & source = _dout_din[y][x];
471  if (source.defined())
472  target = source.to(output_device()).dynamic_expand({neml2::Size(N)});
473  }
474 
475  // clear derivatives
476  _dout_din.clear();
477  }
478  catch (std::exception & e)
479  {
480  mooseError("An error occurred while retrieving outputs from the NEML2 model. Error message:\n",
481  e.what(),
483  }
484 }
485 
486 void
488 {
490  return;
491 
492  // See if any rank failed
493  processor_id_type pid;
495 
496  // Fail the next nonlinear convergence check if any rank failed
497  if (_error)
498  {
500  if (_communicator.rank() == 0)
501  {
502  std::string msg = "NEML2 model execution failed on at least one processor with ID " +
503  std::to_string(pid) + ". Error message:\n";
504  msg += _error_message;
505  if (_fe_problem.isTransient())
506  msg += "\nTo recover, the solution will fail and then be re-attempted with a reduced time "
507  "step.";
508  _console << COLOR_YELLOW << msg << COLOR_DEFAULT << std::endl;
509  }
511  }
512  else if (_t_step > 0)
513  _output_ready = true;
514 }
515 
516 void
518 {
520  mooseError("NEML2 output variables and derivatives must be retrieved during object "
521  "construction. This is a code problem.");
522 }
523 
524 const neml2::Tensor &
525 NEML2ModelExecutor::getOutput(const neml2::VariableName & output_name) const
526 {
528 
529  if (!model().output_variables().count(output_name))
530  mooseError("Trying to retrieve a non-existent NEML2 output variable '", output_name, "'.");
531 
532  return _retrieved_outputs[output_name];
533 }
534 
535 const neml2::Tensor &
536 NEML2ModelExecutor::getOutputDerivative(const neml2::VariableName & output_name,
537  const neml2::VariableName & input_name) const
538 {
540 
541  if (!model().output_variables().count(output_name))
542  mooseError("Trying to retrieve the derivative of NEML2 output variable '",
543  output_name,
544  "' with respect to NEML2 input variable '",
545  input_name,
546  "', but the NEML2 output variable does not exist.");
547 
548  if (!model().input_variables().count(input_name))
549  mooseError("Trying to retrieve the derivative of NEML2 output variable '",
550  output_name,
551  "' with respect to NEML2 input variable '",
552  input_name,
553  "', but the NEML2 input variable does not exist.");
554 
555  return _retrieved_derivatives[output_name][input_name];
556 }
557 
558 const neml2::Tensor &
559 NEML2ModelExecutor::getOutputParameterDerivative(const neml2::VariableName & output_name,
560  const std::string & parameter_name) const
561 {
563 
564  if (!model().output_variables().count(output_name))
565  mooseError("Trying to retrieve the derivative of NEML2 output variable '",
566  output_name,
567  "' with respect to NEML2 model parameter '",
568  parameter_name,
569  "', but the NEML2 output variable does not exist.");
570 
571  if (model().named_parameters().count(parameter_name) != 1)
572  mooseError("Trying to retrieve the derivative of NEML2 output variable '",
573  output_name,
574  "' with respect to NEML2 model parameter '",
575  parameter_name,
576  "', but the NEML2 model parameter does not exist.");
577 
578  return _retrieved_parameter_derivatives[output_name][parameter_name];
579 }
580 
581 #endif
virtual void addGatheredVariable(const UserObjectName &, const neml2::VariableName &)
Register a NEML2 input variable gathered by a gatherer.
neml2::WorkScheduler * scheduler()
Get the work scheduler.
const bool _debug_inputs_on_failure
Dump input tensor info on failure to aid debugging.
std::vector< const MOOSEToNEML2 * > _gatherers
MOOSE data gathering user objects.
A MultiMooseEnum object to hold "execute_on" flags.
Definition: ExecFlagEnum.h:21
static InputParameters validParams()
void paramError(const std::string &param, Args... args) const
Emits an error prefixed with the file and line number of the given param (from the input file) along ...
Definition: MooseBase.h:467
const T & getParam(const std::string &name) const
Retrieve a parameter for the object.
Definition: MooseBase.h:416
void execute() override
Execute method.
neml2::DerivMap _retrieved_derivatives
set of derivatives that were retrieved (by other objects)
bool shouldCompute(const SubProblem &)
Determine whether the NEML2 material model should be evaluated.
Definition: NEML2Utils.C:53
std::vector< const MOOSEToNEML2 * > _param_gatherers
Interface class to provide common input parameters, members, and methods for MOOSEObjects that use NE...
const ExecFlagType & _current_execute_flag
Reference to FEProblemBase.
virtual bool solve()
Perform the material update.
neml2::Model & model() const
Get the NEML2 model.
bool _error
Whether an error was encountered.
const neml2::Device & device() const
Get the target compute device.
processor_id_type rank() const
The main MOOSE class responsible for handling user-defined parameters in almost every MOOSE system...
std::set< neml2::VariableName > _gathered_variable_names
neml2::ValueMap _out
The output variables of the material model.
static std::string NEML2_help_message
Definition: NEML2Utils.h:154
const Parallel::Communicator & _communicator
const ExecFlagType EXEC_TIMESTEP_END
Definition: Moose.C:36
std::set< std::string > _depend_uo
Depend UserObjects that to be used both for determining user object sorting and by AuxKernel for find...
const std::unique_ptr< DispatcherType > & dispatcher() const
Get the work dispatcher.
std::basic_ostream< charT, traits > * os
Definition: InfixIterator.h:34
virtual void extractOutputs()
Extract output derivatives with respect to input variables and model parameters.
registerMooseObject("MooseApp", NEML2ModelExecutor)
void setFailNextNonlinearConvergenceCheck()
Skip further residual evaluations and fail the next nonlinear convergence check(s) ...
InputParameters emptyInputParameters()
auto max(const L &left, const R &right)
bool isEmpty() const
Whether the batch is empty.
ExecFlagEnum getDefaultExecFlagEnum()
Definition: MooseUtils.C:961
std::set< std::string > _gathered_parameter_names
void initialSetup() override
Gets called at the beginning of the simulation before this object is asked to do its job...
const NEML2BatchIndexGenerator & _batch_index_generator
The NEML2BatchIndexGenerator used to generate the element-to-batch-index map.
const neml2::Tensor & getOutputDerivative(const neml2::VariableName &output_name, const neml2::VariableName &input_name) const
Get a reference(!) to the requested output derivative view.
std::string _error_message
Error message.
FEProblemBase & _fe_problem
Reference to the FEProblemBase for this user object.
uint8_t processor_id_type
const std::string & name() const
Get the name of the class.
Definition: MooseBase.h:103
int & _t_step
The number of the time step.
const bool _manage_state_advance
Advance state on device (rather than via MOSOE material properties)
Real value(unsigned n, unsigned alpha, unsigned beta, Real x)
std::map< neml2::VariableName, std::map< std::string, neml2::Tensor > > _retrieved_parameter_derivatives
set of parameter derivatives that were retrieved (by other objects)
virtual void fillInputs()
Fill input variables and model parameters using the gatherers.
void initialize() override
Called before execute() is ever called so that data can be cleared.
void maxloc(T &r, unsigned int &max_id) const
const ExecFlagType EXEC_LINEAR
Definition: Moose.C:31
virtual bool solverSystemConverged(const unsigned int solver_sys_num) override
virtual void checkExecutionStage() const final
Prevent output and derivative retrieval after construction.
neml2::ValueMap _state_vars
Cached variables from the last successful step (for on-device advance)
void broadcast(T &data, const unsigned int root_id=0, const bool identical_sizes=false) const
static InputParameters actionParams()
Parameters that can be specified under the NEML2Action common area.
const ExecFlagType EXEC_NONLINEAR
Definition: Moose.C:33
void advanceState()
Save stateful variables for on-device state advance.
const neml2::Tensor & getOutput(const neml2::VariableName &output_name) const
Get a reference(!) to the requested output view.
std::map< std::string, neml2::Tensor > _model_params
The model parameters to update (gathered from MOOSE)
const neml2::Device & output_device() const
Get the target output device.
NEML2ModelExecutor executes a NEML2 model.
bool _output_ready
flag that indicates if output data has been fully computed
neml2::DerivMap _dout_din
The derivative of the output variables w.r.t. the input variables.
virtual void addGatheredParameter(const UserObjectName &, const std::string &)
Register a NEML2 model parameter gathered by a gatherer.
void meshChanged() override
Called on this object when the mesh changes.
void mooseError(Args &&... args) const
Emits an error prefixed with object name and type and optionally a file path to the top-level block p...
Definition: MooseBase.h:281
static InputParameters validParams()
const neml2::Tensor & getOutputParameterDerivative(const neml2::VariableName &output_name, const std::string &parameter_name) const
Get a reference(!) to the requested output parameter derivative view.
void finalize() override
Finalize.
const ConsoleStream _console
An instance of helper class to write streams to the Console objects.
virtual void validateModel() const
Validate the NEML2 material model.
virtual bool isTransient() const override
auto min(const L &left, const R &right)
virtual void expandInputs()
Expand tensor shapes if necessary to conformal sizes.
std::size_t getBatchIndex() const
Get the current batch index (in almost all cases this is the total batch size)
class infix_ostream_iterator if defined(__GNUC__) &&!defined(__clang__) &&(__GNUC__<
GCC9 currently hits a "no type named &#39;value_type&#39;" error during build if this is removed and iterator...
std::size_t getBatchIndex(dof_id_type elem_id) const
Get the batch index for the given element ID.
neml2::ValueMap _in
The input variables of the material model.
neml2::ValueMap _retrieved_outputs
set of output variables that were retrieved (by other objects)
NEML2ModelExecutor(const InputParameters &params)
uint8_t dof_id_type
virtual bool startedInitialSetup()
Returns true if we are in or beyond the initialSetup stage.
NEML2BatchIndexGenerator iterates over the mesh and generates a map from element ID to batch index wh...
const ExecFlagType EXEC_INITIAL
Definition: Moose.C:30