-
Notifications
You must be signed in to change notification settings - Fork 191
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make process functions submittable #4539
Conversation
tests/engine/test_launch.py
Outdated
# The local calcfunction can not be create, since class can | ||
# not be found when create persistence from local declare function | ||
# inputs = {'x': orm.Int(2)} | ||
# node = launch.submit(local_calcfunction, **inputs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The submit
can not apply to the locally announced calcfunction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is to be expected though. The same holds exactly for all other processes, like WorkChains
. If you define a WorkChain
locally, either in a file that is not in the PYTHONPATH
or let's say an interactive shell, when you submit it, it will fail, because the daemon cannot import the corresponding class.
I think this is great, thanks @unkcpz As you mention, there are restrictions on which process functions can be submitted. In general I don't see a problem with that, it's the same as for other processes. Can we determine in code whether a process function is submittable, or is it something the developer needs to tell us? For some context, in def run_or_submit(self, proc: Process, **kwargs: ty.Any) -> orm.ProcessNode:
if utils.is_process_function(proc):
_, node = run_get_node(proc, **kwargs)
return node
return self.submit(proc, **kwargs) I wouldn't know how to re-write that to take advantage of being able to submit only some process functions. |
Really, for something to be "submittable", simply means that a daemon worker can properly execute the Python code, whether that be a class in the case of a
Note that once process functions are submittable, this will no longer be necessary. Then you can always just call |
Right, my concern is what happens with process functions that are not top-level importable and executable - such as the locally-defined example made by @unkcpz in the tests. I don't really know how common these are - but since that was perfectly legal up until now, I would like my code to continue working with them. Ideally it would also take advantage of being able to |
@sphuber thanks for pointing it out. I just check and find there is are no hints for users when the process is not importable. Is that necessary to make the exceptional info more readable? |
Existing code should not be affected, because currently they can only be run directly in the local interpreter. Since they are defined there, they are importable by definition. Or am I missing something? |
Then I will just add this feature to the docs. Do you have any suggestions of designs or changes I should add in code? |
It is really difficult to do this on the side that is submitting the function. The reason is that it may very well be importable in that code, but the interpreter of the daemon worker that will actually pick it up, may have a different environment where that function is not importable. It is practically impossible to dynamically inspect the environment of a Python interpreter on a different system process to check whether a resource is importable. The problem then is that the exception will appear in the daemon worker as soon as it picks up the process and attempts to start running it. Again, the same problem applies to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @unkcpz . I think the code is fine, straightforward enough. We should just add tests as I described in a code comment
tests/engine/test_launch.py
Outdated
inputs = {'term_a': orm.Int(2), 'term_b': orm.Int(4)} | ||
node = launch.submit(add, **inputs) | ||
self.assertIsInstance(node, orm.CalcFunctionNode) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This merely tests that the submit
method does not except and returns CalcFunctionNode
, which is a good start, but we should also test that the process can actually be executed by a daemon runner. You can try to add this test here, but you will need to fake the daemon runner of course. You can try and take inspiration of the tests/cmdline/commands/test_process.py
tests, or you can add submitting a process function to the .ci/test_daemon.py
script. This will also run in the CI but with a normal daemon running, making it easier to test this kind of stuff. You should test submitting a process function that is defined normally and is importable through the PYTHONPATH
as well as through an entry point. For the latter you can use aiida.workflows:arithmetic.add_multiply
which comes with aiida-core
so you don't have to create one especially for the test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! I add tests in .ci/test_daemon.py
. That's true these 'real' tests bring out a subtle issue, that recreate_from
method should be given to the process function.
aiida/engine/launch.py
Outdated
@@ -96,7 +96,7 @@ def submit(process, **inputs): | |||
:return: the calculation node of the process | |||
:rtype: :class:`aiida.orm.ProcessNode` | |||
""" | |||
assert not is_process_function(process), 'Cannot submit a process function' | |||
# assert not is_process_function(process), 'Cannot submit a process function' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# assert not is_process_function(process), 'Cannot submit a process function' |
It needs to be defined in the interpreter that launches it, not necessarily the local one. That is, it can be defined inside a daemon worker; doesn't mean it is necessarily importable by a different daemon worker. Totally un-advisable example (you can from aiida import orm
from aiida.engine import WorkChain, run_get_node, utils, workfunction
class RunOrSubmitWorkChain(WorkChain):
"""
Adds a 'run_or_submit' method to the WorkChain class, which uses
'run' for process functions and 'submit' else.
"""
def run_or_submit(self, proc, **kwargs):
if utils.is_process_function(proc):
_, node = run_get_node(proc, **kwargs)
return node
return self.submit(proc, **kwargs)
class Sub1(WorkChain):
@classmethod
def define(cls, spec):
super().define(spec)
spec.outline(cls.do_stuff)
def do_stuff(self):
self.report('Sub1 doing stuff.')
class SomeWorkChain(RunOrSubmitWorkChain):
@classmethod
def define(cls, spec):
super().define(spec)
spec.outline(cls.do_stuff)
def do_stuff(self):
@workfunction
def sub_2():
# Capturing 'self' from the 'SomeWorkChain' for added "fun".
self.report("sub 2 doing stuff.")
self.to_context(sub_1=self.run_or_submit(Sub1),
sub_2=self.run_or_submit(sub_2)) To be clear: If it isn't possible to construct a |
I don't think it is possible to create the |
Yeah, agreed - whether the current interpreter can import it is a good indicator, but not a reliable test. It would work in all the examples given here, but fail as soon as Alright now, I'll stop derailing this discussion with my obscure use cases 😅 |
7ce4a6b
to
a8b50a4
Compare
Codecov Report
@@ Coverage Diff @@
## develop #4539 +/- ##
===========================================
+ Coverage 79.49% 79.50% +0.01%
===========================================
Files 482 482
Lines 35323 35326 +3
===========================================
+ Hits 28078 28081 +3
Misses 7245 7245
Flags with carried forward coverage won't be shown. Click here to find out more.
Continue to review full report at Codecov.
|
@sphuber I add the tests and remove the 'process function can not be submitted' warning from the docs. |
Yes, the exact same criterion applies to process functions as for |
Even if it has an entry point, its module still needs to be on the |
I don't think so. As long as there is an entry point that is properly loaded in the |
I think it needs to end up on Whether there is an entry point is a somewhat orthogonal question: If there is one, the (I did a tiny bit of testing, but feel free to double-check this) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @unkcpz a few minor changes requested
.ci/test_daemon.py
Outdated
try: | ||
actual_result = calc.outputs.result | ||
except exceptions.NotExistent: | ||
print(f'Could not retrieve `output_parameters` node for Calculation<{pk}>') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
print(f'Could not retrieve `output_parameters` node for Calculation<{pk}>') | |
print(f'Could not retrieve `result` output for process<{pk}>') |
.ci/test_daemon.py
Outdated
print(f'Submitting {NUMBER_CALCULATIONS} calcfunction to the daemon') | ||
for counter in range(1, NUMBER_CALCULATIONS + 1): | ||
inputval = counter | ||
proc, expected_result = launch_calcfunction(counter=counter, inputval=inputval) | ||
expected_results_calcfunctions[proc.pk] = expected_result | ||
|
||
# Submitting the workfunction through the launchers | ||
print(f'Submitting {NUMBER_CALCULATIONS} workfunction to the daemon') | ||
for counter in range(1, NUMBER_CALCULATIONS + 1): | ||
inputval = counter | ||
proc, expected_result = launch_workfunction(counter=counter, inputval=inputval) | ||
expected_results_workfunctions[proc.pk] = expected_result |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think launching just one of each here is fine. Don't think running multiple process functions tests anything of additional value
@@ -142,6 +142,7 @@ When a process is 'submitted', an instance of the ``Process`` is created, along | |||
This is called a 'process checkpoint', more information on which :ref:`will follow later<topics:processes:concepts:checkpoints>`. | |||
Subsequently, the process instance is shut down and a 'continuation task' is sent to the process queue of RabbitMQ. | |||
This task is simply a small message that just contains an identifier for the process. | |||
In order to reconstruct the process from 'process checkpoint', the process needs to be importable in the daemon environment by a) giving it an associated entry point or b) including its module path in the ``PYTHONPATH`` that the daemon workers will have. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In order to reconstruct the process from 'process checkpoint', the process needs to be importable in the daemon environment by a) giving it an associated entry point or b) including its module path in the ``PYTHONPATH`` that the daemon workers will have. | |
In order to reconstruct the process from a `checkpoint`, the process needs to be importable in the daemon environment by a) giving it an :ref:`associated entry point<how-to:plugin-codes:entry-points>` or b) :ref:`including its module path<how-to:faq:process-not-importable-daemon>` in the ``PYTHONPATH`` that the daemon workers will have. |
You will have to add the line
.. _how-to:faq:process-not-importable-daemon:`
to the file docs/source/howto/faq.rst
on line 65 for the second link
@@ -330,7 +330,7 @@ For example, when we want to run an instance of the :py:class:`~aiida.calculatio | |||
The function will submit the calculation to the daemon and immediately return control to the interpreter, returning the node that is used to represent the process in the provenance graph. | |||
|
|||
.. warning:: | |||
Process functions, i.e. python functions decorated with the ``calcfunction`` or ``workfunction`` decorators, **cannot be submitted** but can only be run. | |||
To submit process, the process needs to be importable, meaning that it either a) has an associated entry point or b) its module path is included in the ``PYTHONPATH`` that the daemon workers will have. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To submit process, the process needs to be importable, meaning that it either a) has an associated entry point or b) its module path is included in the ``PYTHONPATH`` that the daemon workers will have. | |
For a process to be submittable, the class or function needs to be importable in the daemon environment by a) giving it an :ref:`associated entry point<how-to:plugin-codes:entry-points>` or b) :ref:`including its module path<how-to:faq:process-not-importable-daemon>` in the ``PYTHONPATH`` that the daemon workers will have. |
tests/engine/test_launch.py
Outdated
@@ -141,6 +141,24 @@ def test_submit_store_provenance_false(self): | |||
with self.assertRaises(exceptions.InvalidOperation): | |||
launch.submit(AddWorkChain, term_a=self.term_a, term_b=self.term_b, metadata={'store_provenance': False}) | |||
|
|||
def test_submit_process_function(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this was supposed to test submitting a process function, but it not actually does (and this is done isntead in test_daemon.py
, this entire test can be removed.
# process function can be submitted but the process should be importable | ||
# in python interpreter |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# process function can be submitted but the process should be importable | |
# in python interpreter | |
# Process function can be submitted and will be run by a daemon worker as long as the function is importable | |
# Note that the actual running is not tested here but is done so in `.ci/test_daemon.py`. |
submit(self.function_return_true) | ||
# process function can be submitted but the process should be importable | ||
# in python interpreter | ||
from aiida.workflows.arithmetic.add_multiply import add_multiply |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move import to top of file
# process function can be submitted but the process should be importable | ||
# in python interpreter | ||
from aiida.workflows.arithmetic.add_multiply import add_multiply | ||
submit(add_multiply, x=orm.Int(1), y=orm.Int(2), z=orm.Int(3)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
submit(add_multiply, x=orm.Int(1), y=orm.Int(2), z=orm.Int(3)) | |
node = submit(add_multiply, x=orm.Int(1), y=orm.Int(2), z=orm.Int(3)) | |
assert isinstance(node, WorkFunctionNode) |
6e1b645
to
6673cbf
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am afraid I still need one more change, you put the anchor for the docs on the wrong line. Once you correct that I will merge this.
docs/source/howto/faq.rst
Outdated
@@ -77,3 +77,5 @@ Run the command ``verdi daemon logshow`` in a separate terminal to see the loggi | |||
If the root cause is indeed due to an import problem, it will probably appear as an ``ImportError`` exception in the daemon log. | |||
To solve these issues, make sure that all the Python code that is being run is properly importable, which means that it is part of the `PYTHONPATH <https://docs.python.org/3/using/cmdline.html#envvar-PYTHONPATH>`_. | |||
Make sure that the PYTHONPATH is correctly defined automatically when starting your shell, so for example if you are using bash, add it to your ``.bashrc``. | |||
|
|||
.. _how-to:faq:process-not-importable-daemon: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This anchor should be on line 65, just before the header "Why would...."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, fixed. No much experience about rst
;-p
Fix aiidateam#2965, let the process function can be submit to the runner, means shutdown the daemon will not fail the function process (`calcfunction` or the `workfunction`). So they can restart when the daemon is restart. Since the process function still can only running on the local machine, it blocking the daemon even we 'submit' it to the daemon.
Thanks a lot @unkcpz ! |
Feature #2965 to make ProcessFunction submittable, which avoid process functions(which may run for a long time) from interrupt to excepted state when the daemon is shutdown.
However, this means function process(submittable one) should be persisted and can be recreate_from just like
CalcJob
andWorkChain
, i.e. can be imported from the package.So I guess this kind of process function must be declared in the entry_points? Does this violate the basic design of the function process?
pinning you for the comments. @sphuber @muhrin @giovannipizzi