-
-
Notifications
You must be signed in to change notification settings - Fork 46
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
Use Eigen Maps for data and transformed data #865
Conversation
It seems that its having issues with the huge distribution models, causes the AWS instances to run out of memory when compiling. |
Oh really? I'm only seeing
Where are you seeing the distribution model thing? |
You can see it in the previous run (I restarted the runs to check if its a instance/one-off issue) - https://jenkins.mc-stan.org/blue/organizations/jenkins/stanc3/detail/PR-865/5/pipeline/188 and in the old view for the currently running job (https://jenkins.mc-stan.org/job/stanc3/job/PR-865/6/execution/node/215/log/). You can find this under pipeline steps. Its been running for 6 hours now. |
This "Waiting for run to start" seems to be a bug in the Blue Ocean view as its not actually waiting for an instance. |
Ahh icic hmm, it seems to compile all of the example models in performance-tests-cmdstan. And it looks like it only took 5 minutes to compile those. Can we limit the number of parallel jobs running in the compile tests node? |
You can just set -j for this job in the Jenkinsfile if we want to see if it works. |
Alright so compile tests is passing in |
Alrighty so I think I got this all up and working, @t4c1 had a look over the placement new scheme and gave it the thumbs up so if the ocaml looks good I think we are good! |
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 crazy thought: Eigen::Map
can map std::vector
too, right? Would it be possible/reasonable to use X_flat__
in place of X__
to avoid copying? X_flat__
is represented in the MIR and would be a class member if its Decl
wasn't wrapped in a Block
.
./runPerformanceTests.py -j${env.PARALLEL} --runs=0 ../test/integration/good | ||
./runPerformanceTests.py -j7 --runs=0 ../test/integration/good |
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.
Why does this need adjustment?
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 because with the default parallel setting the instances run out of RAM, with -j7 the tests pass but they run for 3 hours instead of 20-30mins! We need to get to the bottom of this.
Does this effect "typical" models or is it just a factor with these huge test models ala this 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.
Yeah I think this is just with the huge models. Though @rok-cesnovar I think you and I have both tried these locally without issue. It could be the compiler we use for running these tests? I'm really not sure. My guess as to what's happening is that we are now generating signatures for both Eigen::Map<>
types and Eigen::Matrix<>
types
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 havent explored it for this PR yet, but have it on my list for the next few days.
match (UnsizedType.is_eigen_type ut, Transform_Mir.is_opencl_var name) with | ||
| true, false -> Some (name, ut) | ||
| false, _ -> None | ||
| true, _ -> None |
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.
match (UnsizedType.is_eigen_type ut, Transform_Mir.is_opencl_var name) with | |
| true, false -> Some (name, ut) | |
| false, _ -> None | |
| true, _ -> None | |
if UnsizedType.is_eigen_type ut && not (Transform_Mir.is_opencl_var name) | |
then Some (name, ut) | |
else None |
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.
Totally cool with this change, though one thing I don't get yet. When is it better to use an if vs. a match? Is it just a style choice? I feel like when I see ifs I usually see us do a ;
at the end I get kind of confused about execution stuff so I'm like "Oh idt I like that" but are ifs totally fine?
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 dunno, I think it's just a style choice. Personally I prefer if
/when
for booleans and match
for anything more complex. Also if
can omit else
so it's a bit more compact if you only need one branch.
If you want to keep match
maybe merge | true, _ -> None | false, _ -> None
into a single | _, _ -> None
?
Actually, thinking more about this, it's more natural to use List.filter
here instead of List.filter_map
.
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.
Makes sense! Switched this to use filter
and if
let nan_type (st, adtype) = | ||
match (adtype, st) with | ||
| UnsizedType.AutoDiffable, _ -> "DUMMY_VAR__" | ||
| DataOnly, SizedType.SInt -> "std::numeric_limits<int>::quiet_NaN()" |
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 does not exist; only floating point types have quiet_NaN
. https://en.cppreference.com/w/cpp/types/numeric_limits/has_quiet_NaN
pp_initialize
ignores this value anyway so this never makes it to the generated code.
let bad_type = Fmt.strf "%a" pp_ut (x, DataOnly) in | ||
raise_s | ||
[%message | ||
"Error during Map data construction for " vident " of type " bad_type |
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.
let bad_type = Fmt.strf "%a" pp_ut (x, DataOnly) in | |
raise_s | |
[%message | |
"Error during Map data construction for " vident " of type " bad_type | |
raise_s | |
[%message | |
"Error during Map data construction for " vident " of type " | |
(x : UnsizedType.t) |
A reasonable thought and good idea! We would move the new (&X) Eigen::Map<Eigen::Matrix<double, -1, 1>>(X_flat__.data(), N, M); Though I think I'd like to do that in a separate PR as I haven't really investigated the transform mir stage (my ocaml-fu is not up to parsing it yet). |
… decls, fix error message and removed quiet_NaN for int
This is ready for another review! |
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 ocaml looks good.
Rad thanks! |
That's great! Can we exclude the vectors/matrices in the data block? or what do we need to make it work? I think |
Since |
Those are the two types this works over so if we remove it for those it's just removing it. If it fails for data I'd guess it would also for for transformed data.
This is kind of a weird error because it implies |
Also is this the right branch for the rstan example above? https://github.com/hsbadr/rstan/tree/StanHeaders_2.26 |
I think so, the variables do exist but likely rejected in the case of
file.remove(system.file("stanc.js", package = "rstan")) Here's the version stamp in my code: // Code generated by stanc v2.26.1-223-g5408944d
I'm on Linux.
No, use the |
Yeah that's the most up to date version of stanc3, let me tinker around with this tmrw I think I can figure this out without breaking upstream things |
Actually I think the error message is coming from here // This context.validate_dims before the actual making of the map
96 : context__.validate_dims("data initialization","x","double",
97 : std::vector<size_t>{static_cast<size_t>(t)});
// Error thrown before getting to map
98 : x__ = Eigen::Matrix<double, -1, 1>(t);
99 : new (&x) Eigen::Map<Eigen::Matrix<double, -1, 1>>(x__.data(), t); Since the message is constructed here https://github.com/stan-dev/stan/blob/develop/src/stan/io/validate_dims.hpp
Still looking into this, could be #871 did something, though that seems odd that this would mess up the var_context somehow |
That makes sense. |
Yes I think I got it, it has to do with this PR but in a very weird way lol. rstan parses the model C++ code to check the model data and the user's data matches up here parsed_data <- with(data, parse_data(get_cppcode(object))) But with this PR that just returns back |
Gotcha! This PR changes the objects parsed from the C++ code. For the example above, it should include |
Yep! Almost finished with a patch I'll send over to you |
Great! The following should work: diff --git a/rstan/rstan/R/misc.R b/rstan/rstan/R/misc.R
index 0038aa8a..13495e00 100644
--- a/rstan/rstan/R/misc.R
+++ b/rstan/rstan/R/misc.R
@@ -1604,6 +1604,8 @@ parse_data <- function(cppcode) {
cppcode[private:public])
# get them from the calling environment
objects <- objects[nzchar(trimws(objects))]
+ objects <- setdiff(objects, "0")
+ objects <- gsub("__$", "", objects)
stuff <- list()
for (int in seq_along(objects)) {
stuff[[objects[int]]] <- dynGet(objects[int], inherits = FALSE, ifnotfound = NULL) |
@SteveBronder Target the |
Word! I posted up a PR but your solution also looks like it would work if that's easier for you |
@SteveBronder ** testing if installed package can be loaded from temporary location
Error: package or namespace load failed for ‘rstanarm’ in dyn.load(file, DLLpath = DLLpath, ...):
unable to load shared object 'rstanarm.so':
rstanarm.so: undefined symbol: _ZN24model_binomial_namespace24csr_matrix_times_vector2IN5Eigen3MapINS1_6MatrixIdLin1ELi1ELi0ELin1ELi1EEELi0ENS1_6StrideILi0ELi0EEEEES4_EENS3_IN5boost4math5tools12promote_argsIN4stan10value_typeIT_vE4typeENSD_IT0_vE4typeEffffE4typeELin1ELi1ELi0ELin1ELi1EEERKiSO_RKSE_RKSt6vectorIiSaIiEESV_RKSH_PSo
Error: loading failed I see |
Are you on the latest version of math? |
Yes. |
Can you send me a script for trying to build what you have locally? My guess is that an old dll is not being recompiled with the newest stan math version. It probably has something to do with stan-dev/math#2462 where I added an autodiff specialization for |
Just install file.remove(system.file("stanc.js", package = "rstan")) The compilation is successful, but it fails on loading. |
I'll test it after reverting this Math PR and will let you know. |
This doesn't fix the problem. I think it's related to |
Okay I'm trying to fix my home desktop rn but I'll look at this after I get that all done |
Ah! So I'm also getting
The 2 at the end of |
@hsbadr I put up a patch in stan-dev/rstanarm#514 that should fix this |
That makes sense.
Thanks! I'll test your patch. |
Summary
Eigen matrices and vectors used by the model are now
Eigen::Map
types.std::vector<Eigen>
types will still continue to be the same as they are now. This PR differs from the original PR in that now instead of using a local stack allocator we just use the memory made by the original data.Description
So this PR requires a couple little explainers.
First, the models class members now includes
Eigen::Map<EigenType>
s of each of the user specified matrices. For example in the diamond.stan model the members are nowSo all the data now has
__
after it and the user specified data names are now the names of theEigen::Map<>
member types. That{nullptr, 0}
probably seem very odd, but Eigen's rules around maps require them to be initialized either by a default or in the initializer list of the class constructor. For our purposes, we only need these members to be initialized and have the correct memory size. An Eigen map is pretty simple, it has a pointer and some integers in it like the following pseudoclass.To make these Eigen maps actually reference the memory we want we'll use placement new in the model class's constructor body. By calling
new (*EigenMap) Map(RealData...)
we can make a new Map, overwriting our initially default constructed maps memory, that has access to the actual memory we want! That looks like the belowHuh! So all that's going on here is that we are allocating memory for
Y__
on the first line, then we use placement new, passing it the pointer to our member mapY
to construct a new map who knows about thatY__
's memory + size, then for the rest of the program we treatY
as if it was the original matrix that is nowY__
.The results from the comment here hold which is nice and I tested compiling a few models locally which seemed to work.