-
Notifications
You must be signed in to change notification settings - Fork 394
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
Allow Plugin Plugins in Runtime API Calls #8913
Conversation
Regression diffs are purely because the branch was out of date. I'll fix any documentation needed and pull develop in and it will be clean again. |
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.
Code walkthrough - pretty minimal.
src/EnergyPlus/PluginManager.cc
Outdated
@@ -427,6 +427,10 @@ PluginManager::PluginManager(EnergyPlusData &state) | |||
// If arg 0, it skips init registration of signal handlers, which might be useful when Python is embedded. | |||
Py_InitializeEx(0); | |||
|
|||
// Take control of the global interpreter lock while we are here, make sure to release it... | |||
PyGILState_STATE s = PyGILState_Ensure(); |
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.
Hold a reference to the global interpreter lock while we instantiate the plugins at the beginning of the program.
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.
Just a nitpick, but I would caution to use a better, more explicit name, for the variable. like gil_state or something.
I could see s
being used for a dumb string at some point.
src/EnergyPlus/PluginManager.cc
Outdated
@@ -583,6 +587,8 @@ PluginManager::PluginManager(EnergyPlusData &state) | |||
} | |||
} | |||
|
|||
// Release the global interpreter lock | |||
PyGILState_Release(s); |
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.
Release the GIL once we are done interpreting the plugin.
src/EnergyPlus/PluginManager.cc
Outdated
@@ -1021,6 +1027,9 @@ bool PluginInstance::run(EnergyPlusData &state, EMSManager::EMSCallFrom iCalledF | |||
return false; | |||
} | |||
|
|||
// Get control of the global interpreter lock | |||
PyGILState_STATE s = PyGILState_Ensure(); |
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.
Hold a reference to the GIL while we execute the plugin.
src/EnergyPlus/PluginManager.cc
Outdated
@@ -1034,6 +1043,7 @@ bool PluginInstance::run(EnergyPlusData &state, EMSManager::EMSCallFrom iCalledF | |||
} else { | |||
EnergyPlus::ShowContinueError(state, "This could happen for any number of reasons, check the plugin code."); | |||
} | |||
PyGILState_Release(s); |
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.
Make sure to release the GIL on each code path out of this function. There are several fatal error paths, so this is on each of them.
ShowFatalError(state, "Invalid Python Plugin object encounter causes program termination"); | ||
} | ||
} | ||
state.dataPluginManager->pluginManager = std::make_unique<EnergyPlus::PluginManagement::PluginManager>(state); |
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.
Loosen the constraint here. May want to eliminate the eplusRunningViaAPI
variable since I don't think it should matter anymore.
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.
Scratch that, don't delete it...a later branch wants to use this.
@Myoldmopar Could you clarify something for me please? I understand the intent: being able to launch via API an E+ sim which itself have PythonPlugin:Instance objects. But, if from python I make a thread pool, and run like 8 distinct E+ simulations... Do they still run in parallel? I think that would be the case if I used threading.Thread, but not if I use multiprocessing.Pool probably, since the latter spaws a distinct python interpreter and memory space. (I would have used multiprocessing.Pool for this anyways TBH) |
src/EnergyPlus/PluginManager.cc
Outdated
@@ -427,6 +427,10 @@ PluginManager::PluginManager(EnergyPlusData &state) | |||
// If arg 0, it skips init registration of signal handlers, which might be useful when Python is embedded. | |||
Py_InitializeEx(0); | |||
|
|||
// Take control of the global interpreter lock while we are here, make sure to release it... | |||
PyGILState_STATE s = PyGILState_Ensure(); |
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.
Just a nitpick, but I would caution to use a better, more explicit name, for the variable. like gil_state or something.
I could see s
being used for a dumb string at some point.
@jmarrec great question! I tested it out and have some quick results. Reference CaseI took 5ZoneAirCooled and ran it in a single instance and then 7 parallel threads:
Python Thread CaseI took PythonPluginCustomSchduled and did the same run with threads:
Python Multiprocessing CaseI then created a simple multiprocessing pool and ran the same Python Plugin file:
SummaryHowever, this isn't just a free win. One of the key aspects of running via API is that you can, within the same process, spin up multiple EnergyPlus threads and have them talk to each other. This is a very easy task with multithreading because you can share the same Python variables and have the threads talk back in the same process space while they are running. That's not so easy with multiprocessing (someone prove me wrong). However Number 2: If someone is going to be spinning up multiple E+ threads to run in parallel and ask them to be talking to each other, are they really also going to be asking each one to go out and run Python Plugins as well? I honestly can't imagine that to be the case. If they are tying the simulations together, then I would suspect that they are just using Python callbacks do the the integration and not also creating Python plugin files. I could be wrong. But if they are, then this workflow will still work, they will just notice some slowdown. The real purpose of this PR is to simply allow interfaces that want to run E+ from their Python-based GUI, including sometimes running files that have Python Plugins. I think the overlap of Python GUI, Plugin, and cosimulation is probably a very, very small corner case for now. And, once again, we allow it, there is just a performance penalty for that tight integration. Maybe soon Python will allow first-class sub-interpreters and we'll be able to do this without a penalty... 👍 |
I am going to proceed on polishing up this branch, updating docs to remove any notes about how API and Plugins are incompatible, adding a test where we call a plugin file from an API test, and also changing the GIL variable name per @jmarrec request. If there are other comments, please speak up, otherwise this branch should be ready to go once I push later today. |
Pulled develop in and tweaked documentation. I don't know that I'll add even more testing in this PR, but it will be in the release package tester for sure. |
setConsoleOutputState(state2, 0); | ||
energyplus(state2, argc, argv); | ||
printf("...and it is done."); | ||
EnergyPlusState state3 = stateNew(); // stateReset(state); |
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.
Something a little funny was happening in the reused state so this just uses individual instances for now to get this running. I'll take a look later.
This branch was clean the last time around and it is still up to date with develop, so I'm merging it in. |
Pull request overview
Background:
Up until now, there has been a limitation: when calling EnergyPlus as a library, a client could not run a file that executed a Python Plugin. If you were already inside a thread running Python when you called into EnergyPlus, then setting up a new Python interpreter and executing it internally would cause a segmentation fault. This was the state until this branch. I knew there had to be a means to make this work, and did some deeper research.
For this PR, I am gaining control of Python's global interpreter lock while inside EnergyPlus. This essentially localizes the internal interpreter operations so that it doesn't muck around with any Python interpreter that may be calling EnergyPlus from the outside. I have tested this in a number of ways, and asked @mitchute to test it out in a more complex environment where he is using third party Python dependencies, and everything seems to work just fine. We can now allow library calls that execute input files that have Python Plugins.
This was a great concern of a prominent interface developer when looking at using EnergyPlus API. And also came up as we convert EP-Launch to make API calls instead of calling EnergyPlus as a standalone executable.
To do:
Potentially removeeplusRunningViaAPI
variable and any accompanying API layer stuff.