-
Notifications
You must be signed in to change notification settings - Fork 60
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
Add pythonizations for collection subscript #570
Conversation
If I understand correctly this would effectively give an "opt-in" solution for generated EDMs, as the EDM has to explicitly load these pythonizations. For EDM4hep this would not really be a problem, as we have some python glue code already in any case. For me this looks like a good solution, but we should add a bit of documentation on how to use this in generated EDMs. |
yes, it's opt-in.
Thanks, I will add it |
Note that this will make indexing significantly slower: In [2]: import edm4hep
In [3]: coll = edm4hep.MCParticleCollection()
In [4]: coll.create()
Out[4]: <cppyy.gbl.edm4hep.MutableMCParticle object at 0x55f984f08120>
In [5]: %timeit coll[0]
3.45 µs ± 72.6 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each) and without this PR:
It should be said somewhere that the current behaviour is you get an object when indexing out of bounds and then it crashes when you try to do something with that object. But I think the case of indexing out of bounds should be rare since typically you run either a for loop without indexes or if it has indexes then it's bounded by |
Thanks for pointing this out, I'll check if this can be improved by mapping |
I tried benchmarking on following function: def bench():
try:
coll[0]
except:
pass
coll = MCParticleCollection()
bench() # give cppyy time to load lazily
%timeit bench()
coll.create()
bench() # give cppyy time to load lazily
%timeit bench()
The implementations using A C++ callback added to the Collection template could potentially achieve the same as The performance of |
I see there has been quite some discussion (and progress?) here. A few comments / questions from my side in no particular order:
|
Pure C++ google benchmark between a single element access with ----------------------------------------------------------------
Benchmark Time CPU Iterations
----------------------------------------------------------------
squareBracketsNoError 4.59 ns 4.59 ns 152241304
atNoError 5.26 ns 5.25 ns 131108856
atError 1552 ns 1550 ns 452227 where static void squareBracketsNoError(benchmark::State& state) {
edm4hep::MCParticleCollection coll;
coll.create();
for (auto _ : state) {
try {
benchmark::DoNotOptimize(coll[0]);
benchmark::ClobberMemory();
} catch (const std::out_of_range&) {
}
}
}
static void atNoError(benchmark::State& state) {
edm4hep::MCParticleCollection coll;
coll.create();
for (auto _ : state) {
try {
benchmark::DoNotOptimize(coll.at(0));
benchmark::ClobberMemory();
} catch (const std::out_of_range&) {
}
}
}
static void atError(benchmark::State& state) {
edm4hep::MCParticleCollection coll;
coll.create();
for (auto _ : state) {
try {
benchmark::DoNotOptimize(coll.at(1));
benchmark::ClobberMemory();
} catch (const std::out_of_range&) {
}
}
} both this and python benchmark are isolated single access, looping over the collection will probably give different results
Yes, all the alternatives from that table were python callbacks
In the last commit I changed the implementation proposed in PR to option 4 from the table. It seems to me as a reasonable compromise |
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 for also checking the c++ times. The orders of magnitude difference between c++ and python looks like expected, I would say.
I would also vote for option 4 here. I just have one minor question below.
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 functionality should be in podio. For EDM4hep, I would like to have only what's necessary since there can be some drawbacks for interactive usage
Can you rebase this onto the latest master to pick up the RNTuple API fixes? That should also make CI pass again. |
Co-authored-by: Juan Miguel Carceller <22276694+jmcarcell@users.noreply.github.com>
Co-authored-by: Thomas Madlener <thomas.madlener@desy.de>
Sorry for the delay. Rebased and added documentation. I also split the callback into two functions:
I think previously it was not clear that callback should start with some conditional statement to select what should be modified |
with self.assertRaises(IndexError): | ||
_ = collection[20] |
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 fails on dev3
due to some overload resolution issue. I would assume it's a ROOT issue that we are hitting here, but I am not sure yet which one.
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.
Well an exception is thrown, but of a different type than expected:
Traceback (most recent call last):
File "/home/runner/work/podio/podio/python/podio/test_CodeGen.py", line 50, in test_bound_check
_ = collection[20]
File "/home/runner/work/podio/podio/python/podio/pythonizations/collection_subscript.py", line 22, in get_item
return self.at(i)
TypeError: none of the 2 overloaded methods succeeded. Full details:
nsp::EnergyInNamespace nsp::EnergyInNamespaceCollection::at(size_t index) =>
out_of_range: deque::_M_range_check: __n (which is 20)>= this->size() (which is 1)
nsp::MutableEnergyInNamespace nsp::EnergyInNamespaceCollection::at(size_t index) =>
out_of_range: deque::_M_range_check: __n (which is 20)>= this->size() (which is 1)
According to the docs if there is an exception then cppyy checks the rest of overloads and if they all throw the same type then the exception is propagated, if they all throw different types then TypeError
is risen
Here both overloads throws std::out_of_range
but somehow it isn't recognized as the same type
The possible solutions I could think of now are:
- explicitly select overload - here the overloads are const and non-const. Selection between const and non-const was added in cppyy-1.7.0, nightlies have 1.6.2
- change pythonization to catch either
cppyy.gbl.std.out_of_range
orTypeError
and raiseIndexError
- I don't like it becasue it's also used for other things, like actual type mismatch, eg. calling withstr
instead ofint
- allow both types of exception in test - getting an exception is already an improvement, the message says it's out of range. The users get confused why it's
TypeError
notIndexError
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.
Maybe this can be boiled down to a small reproducer that we can submit to ROOT because they probably didn't want to introduce too many "breaking" changes in their update of the cppyy they bundle.
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.
Opened an issue wlav/cppyy#230 as the problems appears also outside of ROOT bundle
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 also made the ROOT folks aware via root-project/root#15375
Co-authored-by: Thomas Madlener <thomas.madlener@desy.de>
Looks like all the necessary fixes have landed in upstream root (master and v6.32). IIRC this can be merged now, right? |
Awesome 😎 Yes, it's ready to go |
and off we go... |
BEGINRELEASENOTES
ENDRELEASENOTES
#458 and key4hep/EDM4hep#288 show there is some interest in tuning the cppyy generated bindings. The cppyy has a feature for this called pythonization -> a callback can be registered for all the classes that fulfill some predicate (e.g. derived from
podio::CollectionBase
).This PR contains:
podio.pythonizations.utils.Pythonizer
)__getitem__
for classes derived frompodio::CollectionBase
The mechanism of loading the pythonizations is inspired by ROOT pythonizations with a change that de order of loading of the pythonizations is defined.
To load the pythonizations in a downstream projects, for instance for project utilizing a "edm4hep" namespace
alternatively a selection of pythonizations can be loaded by importing and applying them one by one
The alternative mechanism I tough about:
Python::Module
) and knowing the Python C API is required