-
-
Notifications
You must be signed in to change notification settings - Fork 48
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
Call eval on UDF arguments which are indices in UDF bodies #1228
Conversation
I went with the safe but possibly over-eager choice to evaluate whenever a function call in a function body has the same name as the function itself. As noted in the test and in the discussion in #1224, this is not necessarily a recursive call in the context of overloading. So, this may produce a few extra calls to eval than it needs to. |
Unfortunately this misses mutually recursive functions. functions {
vector test3(vector gamma);
vector test2(vector gamma);
vector test2(vector gamma) {
int D = num_elements(gamma);
if (D == 1)
return rep_vector(D, 0);
else
return test3(gamma[1:D - 1]);
}
vector test3(vector gamma) {
return test2(gamma);
}
}
data {
int N;
int times;
}
parameters {
vector[times] gamma;
}
transformed parameters {
vector[N] z_hat = test2(gamma);
} |
Yes, it does. I don't think there's any good way around that short of re-implementing most of the C++ compiler's template resolution pass? Or we could eval all indexed arguments to UDF calls within UDFs. Even if we don't fix the mutually recursive case, I think this change is still an improvement relative to the current behavior |
Updated to
|
Fixing it fully is indeed difficult and even a partial solution is an improvement, yes. I think the ideal solution would be to do a topological sort and build a DAG whose vertices are functions and edges are calls, and if there are cycles then break them by inserting |
I think the expense of eval will vary depending on the usage. In the simplest of cases, say where the function only cares about the length of the vector but not the values, it will be doing an extra copy each time as a result of the @rok-cesnovar or @SteveBronder may be able to say more about this |
Eigen uses the RAII pattern, so an eval will cause an allocation and copy. So both memory and CPU pressure.
Eigen will only implicitly evaluate an argument if it decides it's more efficient that way. Typical uses that only access values once, like a dot product, won't force an eval. |
In the short term I think the extra evals are a worthwhile tradeoff for safety. I expect this is probably not super commonly written code by users where UDFs are calling other UDFs with slices of eigen types. We can later try a more sophisticated analysis as to when they are necessary. I think the proposal @nhuurre makes for that is the correct approach, but implementing that would be nontrivial. |
e.g. it is forwardly declared
I think my latest commit will be the happy-middle of the current options. I've restricted the extra |
@nhuurre thoughts on the current state of this? I discussed with @rok-cesnovar and we are going to do a minor release with just the recent stanc bug fixes (hopefully including this), Cherry-picking around the to_int and new warnings |
match (pattern, UnsizedType.is_eigen_type (Expr.Typed.type_of expr)) with | ||
| Indexed _, true -> | ||
{meta; pattern= FunApp (StanLib ("eval", FnPlain, AoS), [expr])} | ||
| _ -> expr in |
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.
Indexed
is not enough. Some math lib functions also fail.
vector test2(vector gamma) {
int D = num_elements(gamma);
if (D == 1)
return rep_vector(D, 0);
else
return test2(head(gamma,D - 1));
}
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.
It's easy enough to also wrap arbitrary math functions, but figuring out an exhaustive list of which ones will trigger this behavior seems daunting. It's probably reasonable to start with the functions listed here? If we get reports or find more in the future it is easy enough to add them
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.
LGTM
Call eval on UDF arguments which are indices in UDF bodies
Closes #1224
Submission Checklist
Release notes
Fix an issue where recursive functions which called themselves with a slice of their arguments would lead to infinite template expansion in C++ compilation.
Copyright and Licensing
By submitting this pull request, the copyright holder is agreeing to
license the submitted work under the BSD 3-clause license (https://opensource.org/licenses/BSD-3-Clause)