https://mooseframework.inl.gov
Loading...
Searching...
No Matches
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
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
97void
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
147std::size_t
148NEML2ModelExecutor::getBatchIndex(dof_id_type elem_id) const
149{
151}
152
153void
154NEML2ModelExecutor::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
167void
168NEML2ModelExecutor::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
181void
183{
185 return;
186
187 _output_ready = false;
188 _error = false;
189 _error_message.clear();
190}
191
192void
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
205void
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)
229 }
230}
231
232void
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
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
270void
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
285void
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
308bool
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
441void
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
486void
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;
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
516void
518{
520 mooseError("NEML2 output variables and derivatives must be retrieved during object "
521 "construction. This is a code problem.");
522}
523
524const neml2::Tensor &
525NEML2ModelExecutor::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
535const neml2::Tensor &
536NEML2ModelExecutor::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
558const neml2::Tensor &
559NEML2ModelExecutor::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
InputParameters emptyInputParameters()
void mooseError(Args &&... args)
Emit an error message with the given stringified, concatenated args and terminate the application.
Definition MooseError.h:311
const ExecFlagType EXEC_TIMESTEP_END
Definition Moose.C:36
const ExecFlagType EXEC_INITIAL
Definition Moose.C:30
const ExecFlagType EXEC_LINEAR
Definition Moose.C:31
const ExecFlagType EXEC_NONLINEAR
Definition Moose.C:33
unsigned int count
Definition MortarUtils.C:53
registerMooseObject("MooseApp", NEML2ModelExecutor)
if(!dmm->_nl) SETERRQ(PETSC_COMM_WORLD
const ConsoleStream _console
An instance of helper class to write streams to the Console objects.
A MultiMooseEnum object to hold "execute_on" flags.
void setFailNextNonlinearConvergenceCheck()
Skip further residual evaluations and fail the next nonlinear convergence check(s)
virtual bool solverSystemConverged(const unsigned int solver_sys_num) override
virtual bool isTransient() const override
virtual bool startedInitialSetup()
Returns true if we are in or beyond the initialSetup stage.
The main MOOSE class responsible for handling user-defined parameters in almost every MOOSE system.
const std::string & name() const
Get the name of the class.
Definition MooseBase.h:103
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:457
const T & getParam(const std::string &name) const
Retrieve a parameter for the object.
Definition MooseBase.h:406
NEML2BatchIndexGenerator iterates over the mesh and generates a map from element ID to batch index wh...
bool isEmpty() const
Whether the batch is empty.
std::size_t getBatchIndex() const
Get the current batch index (in almost all cases this is the total batch size)
NEML2ModelExecutor executes a NEML2 model.
std::map< neml2::VariableName, std::map< std::string, neml2::Tensor > > _retrieved_parameter_derivatives
set of parameter derivatives that were retrieved (by other objects)
neml2::DerivMap _retrieved_derivatives
set of derivatives that were retrieved (by other objects)
void initialSetup() override
Gets called at the beginning of the simulation before this object is asked to do its job.
const bool _debug_inputs_on_failure
Dump input tensor info on failure to aid debugging.
neml2::ValueMap _out
The output variables of the material model.
bool _error
Whether an error was encountered.
void execute() override
Execute method.
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.
static InputParameters actionParams()
Parameters that can be specified under the NEML2Action common area.
virtual bool solve()
Perform the material update.
std::set< std::string > _gathered_parameter_names
neml2::ValueMap _retrieved_outputs
set of output variables that were retrieved (by other objects)
neml2::ValueMap _in
The input variables of the material model.
std::vector< const MOOSEToNEML2 * > _param_gatherers
virtual void checkExecutionStage() const final
Prevent output and derivative retrieval after construction.
static InputParameters validParams()
const bool _manage_state_advance
Advance state on device (rather than via MOSOE material properties)
bool _output_ready
flag that indicates if output data has been fully computed
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.
virtual void addGatheredVariable(const UserObjectName &, const neml2::VariableName &)
Register a NEML2 input variable gathered by a gatherer.
std::size_t getBatchIndex(dof_id_type elem_id) const
Get the batch index for the given element ID.
void advanceState()
Save stateful variables for on-device state advance.
void finalize() override
Finalize.
std::map< std::string, neml2::Tensor > _model_params
The model parameters to update (gathered from MOOSE)
const NEML2BatchIndexGenerator & _batch_index_generator
The NEML2BatchIndexGenerator used to generate the element-to-batch-index map.
neml2::DerivMap _dout_din
The derivative of the output variables w.r.t. the input variables.
virtual void extractOutputs()
Extract output derivatives with respect to input variables and model parameters.
void initialize() override
Called before execute() is ever called so that data can be cleared.
neml2::ValueMap _state_vars
Cached variables from the last successful step (for on-device advance)
virtual void fillInputs()
Fill input variables and model parameters using the gatherers.
NEML2ModelExecutor(const InputParameters &params)
const neml2::Tensor & getOutput(const neml2::VariableName &output_name) const
Get a reference(!) to the requested output view.
virtual void expandInputs()
Expand tensor shapes if necessary to conformal sizes.
std::vector< const MOOSEToNEML2 * > _gatherers
MOOSE data gathering user objects.
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.
std::set< neml2::VariableName > _gathered_variable_names
Interface class to provide common input parameters, members, and methods for MOOSEObjects that use NE...
static InputParameters validParams()
virtual void validateModel() const
Validate the NEML2 material model.
neml2::Model & model() const
Get the NEML2 model.
neml2::WorkScheduler * scheduler()
Get the work scheduler.
const neml2::Device & output_device() const
Get the target output device.
const std::unique_ptr< DispatcherType > & dispatcher() const
Get the work dispatcher.
const neml2::Device & device() const
Get the target compute device.
const ExecFlagType & _current_execute_flag
Reference to FEProblemBase.
void maxloc(T &r, unsigned int &max_id) const
processor_id_type rank() const
void broadcast(T &data, const unsigned int root_id=0, const bool identical_sizes=false) const
int & _t_step
The number of the time step.
std::set< std::string > _depend_uo
Depend UserObjects that to be used both for determining user object sorting and by AuxKernel for find...
FEProblemBase & _fe_problem
Reference to the FEProblemBase for this user object.
const Parallel::Communicator & _communicator
ExecFlagEnum getDefaultExecFlagEnum()
Definition MooseUtils.C:972
bool shouldCompute(const SubProblem &)
Determine whether the NEML2 material model should be evaluated.
Definition NEML2Utils.C:53
static std::string NEML2_help_message
Definition NEML2Utils.h:154
NumberTensorValue Tensor