-
-
Notifications
You must be signed in to change notification settings - Fork 63
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
MPI sampling #350
MPI sampling #350
Conversation
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.
Question/TODO for now:
- do we want a separate
$mpi_sample()
? I now think we could also work with$sample()
- names for the number of MPI processes and mpi command arguments? Ideas welcome.
- CI, need to set up a separate test for this
- figure out why
mod_mpi <- cmdstan_model("logistic1.stan", cpp_options = list("CXX"="mpicxx", stan_mpi = TRUE, "TBB_CXX_TYPE"="gcc"))
fails. It should not.
is this something folks want in Python as well? |
No idea honestly. MPI is a part of cmdstan and I figure we should support it. Probably no one will use cmdstanx on a cluster, but I think its worth it to support stuff like the cross-chain warmup Yi et al are working on. Especially given that it does really not seem to be that big of a maintenance burden (famous last words). |
Yeah I don't really know either. But yeah fine by me to support it if it's in CmdStan (and isn't getting deprecated anytime soon).
Curious, what makes you think that? |
I think there is a big enough user base and use case that this is not going to happen.
Mostly due to the way jobs are submitted to the typical cluster via job submission scripts. |
mmm, on develop branch of cmdstan I'm getting /Users/yiz/Work/temp/cmdstan/stan/lib/stan_math/lib/boost_1.72.0/tools/build/src/build/toolset.jam:44: in toolset.using
ERROR: rule "other.init" unknown in module "toolset".
/Users/yiz/Work/temp/cmdstan/stan/lib/stan_math/lib/boost_1.72.0/tools/build/src/build-system.jam:543: in process-explicit-toolset-requests
/Users/yiz/Work/temp/cmdstan/stan/lib/stan_math/lib/boost_1.72.0/tools/build/src/build-system.jam:610: in load
/Users/yiz/Work/temp/cmdstan/stan/lib/stan_math/lib/boost_1.72.0/tools/build/src/kernel/modules.jam:295: in import
/Users/yiz/Work/temp/cmdstan/stan/lib/stan_math/lib/boost_1.72.0/tools/build/src/kernel/bootstrap.jam:139: in boost-build
/Users/yiz/Work/temp/cmdstan/stan/lib/stan_math/lib/boost_1.72.0/boost-build.jam:17: in module scope
other.jam: No such file or directory when running library(cmdstanr)
set_cmdstan_path("cmdstan")
cmdstan_make_local(cpp_options = list("CXX"="mpicxx", "TBB_CXX_TYPE"="gcc"))
rebuild_cmdstan(cores = 4) |
You had TBB_CXX_TYPE=clang for your system I believe. Might be that? |
You're right. |
why do you say this? people who are used to working in R or Python will use those languages to set up job submission accordingly - see Discourse discussion - https://discourse.mc-stan.org/t/correct-way-to-use-mpi-with-cmdstanpy/17667 - I wrote an example of job submission script - https://discourse.mc-stan.org/t/correct-way-to-use-mpi-with-cmdstanpy/17667/2?u=mitzimorris |
Funny, I'm still getting the same error with cmdstan master: bash-3.2$ git branch
WARNING: terminal is not fully functional
- (press RETURN)
develop
* master
bash-3.2$ cat make/local
STAN_MPI=1
CXX=mpicxx
TBB_CXX_TYPE=clang
bash-3.2$ make clean-all;make -j4 build
rm -f -r test
rm -f
rm -f
rm -f
rm -f
removing dependency files
rm -f
rm -f
rm -f
cleaning sundials targets
rm -f
cleaning mpi targets
rm -f
rm -f -r stan/lib/stan_math/lib/boost_1.72.0/stage/lib stan/lib/stan_math/lib/boost_1.72.0/project-config.jam stan/lib/stan_math/lib/boost_1.72.0/b2 stan/lib/stan_math/lib/boost_1.72.0/bootstrap.log
cleaning Intel TBB targets
rm -f -rf stan/lib/stan_math/lib/tbb
rm -f bin/stanc bin/stanc2 bin/stansummary bin/print bin/diagnose
rm -f -r src/cmdstan/main*.o bin/cmdstan
rm -f
rm -f examples/bernoulli/bernoulli examples/bernoulli/bernoulli.o examples/bernoulli/bernoulli.d examples/bernoulli/bernoulli.hpp
rm -f -r stan/lib/stan_math/lib/boost_1.72.0/stage/lib stan/lib/stan_math/lib/boost_1.72.0/project-config.jam stan/lib/stan_math/lib/boost_1.72.0/b2 stan/lib/stan_math/lib/boost_1.72.0/bootstrap.log
curl -L https://github.com/stan-dev/stanc3/releases/download/nightly/mac-stanc -o bin/stanc --retry 5 --retry-delay 10
mpicxx -std=c++1y -D_REENTRANT -Wno-ignored-attributes -Wno-delete-non-virtual-dtor -I stan/lib/stan_math/lib/tbb_2019_U8/include -O3 -I src -I stan/src -I lib/rapidjson_1.1.0/ -I stan/lib/stan_math/ -I stan/lib/stan_math/lib/eigen_3.3.7 -I stan/lib/stan_math/lib/boost_1.72.0 -I stan/lib/stan_math/lib/sundials_5.2.0/include -DBOOST_DISABLE_ASSERTS -c -fvisibility=hidden -o bin/cmdstan/stansummary.o src/cmdstan/stansummary.cpp
cd stan/lib/stan_math/lib/boost_1.72.0; ./bootstrap.sh
mpicxx -std=c++1y -D_REENTRANT -Wno-ignored-attributes -Wno-delete-non-virtual-dtor -I stan/lib/stan_math/lib/tbb_2019_U8/include -O3 -I src -I stan/src -I lib/rapidjson_1.1.0/ -I stan/lib/stan_math/ -I stan/lib/stan_math/lib/eigen_3.3.7 -I stan/lib/stan_math/lib/boost_1.72.0 -I stan/lib/stan_math/lib/sundials_5.2.0/include -DBOOST_DISABLE_ASSERTS -c -fvisibility=hidden -o bin/cmdstan/print.o src/cmdstan/print.cpp
% Total % Received % Xferd Average Speed Time Time Time Current
Building Boost.Build engine with toolset clang... Dload Upload Total Spent Left Speed
100 635 100 635 0 0 1951 0 --:--:-- --:--:-- --:--:-- 1947
100 10.4M 100 10.4M 0 0 4810k 0 0:00:02 0:00:02 --:--:-- 8840k
chmod +x bin/stanc
mpicxx -std=c++1y -D_REENTRANT -Wno-ignored-attributes -Wno-delete-non-virtual-dtor -I stan/lib/stan_math/lib/tbb_2019_U8/include -O3 -I src -I stan/src -I lib/rapidjson_1.1.0/ -I stan/lib/stan_math/ -I stan/lib/stan_math/lib/eigen_3.3.7 -I stan/lib/stan_math/lib/boost_1.72.0 -I stan/lib/stan_math/lib/sundials_5.2.0/include -DBOOST_DISABLE_ASSERTS -c -fvisibility=hidden -o bin/cmdstan/diagnose.o src/cmdstan/diagnose.cpp
tools/build/src/engine/b2
Detecting Python version... 2.7
Detecting Python root... /usr/local/Cellar/python@2/2.7.15/Frameworks/Python.framework/Versions/2.7
Unicode/ICU support for Boost.Regex?... not found.
Generating Boost.Build configuration in project-config.jam for clang...
Bootstrapping is done. To build, run:
./b2
To generate header files, run:
./b2 headers
To adjust configuration, edit 'project-config.jam'.
Further information:
- Command line help:
./b2 --help
- Getting started guide:
http://www.boost.org/more/getting_started/unix-variants.html
- Boost.Build documentation:
http://www.boost.org/build/
cd stan/lib/stan_math/lib/boost_1.72.0; ./b2 toolset=other --visibility=hidden --with-program_options cxxstd=11 variant=release link=static
other.jam: No such file or directory
/Users/yiz/Work/temp/cmdstan/stan/lib/stan_math/lib/boost_1.72.0/tools/build/src/build/toolset.jam:44: in toolset.using
ERROR: rule "other.init" unknown in module "toolset".
/Users/yiz/Work/temp/cmdstan/stan/lib/stan_math/lib/boost_1.72.0/tools/build/src/build-system.jam:543: in process-explicit-toolset-requests
/Users/yiz/Work/temp/cmdstan/stan/lib/stan_math/lib/boost_1.72.0/tools/build/src/build-system.jam:610: in load
/Users/yiz/Work/temp/cmdstan/stan/lib/stan_math/lib/boost_1.72.0/tools/build/src/kernel/modules.jam:295: in import
/Users/yiz/Work/temp/cmdstan/stan/lib/stan_math/lib/boost_1.72.0/tools/build/src/kernel/bootstrap.jam:139: in boost-build
/Users/yiz/Work/temp/cmdstan/stan/lib/stan_math/lib/boost_1.72.0/boost-build.jam:17: in module scope
make: *** [stan/lib/stan_math/lib/boost_1.72.0/stage/lib/libboost_program_options.a] Error 1
make: *** Waiting for unfinished jobs.... |
You dont neeed to set stan_mpi before rebuilding. You can just use it when compiling the model. Will look into why its failing on rebuild though. |
As of 2.24 its no longer required to rebuild cmdstan upon setting the mpi/threads/opencl flags. If you set them for models, the main.o is rebuilt automatically. |
I don't think it's caused by rebuilding or CXX=mpicxx
TBB_CXX_TYPE=clang |
Ok, let me check that.This error on make build doesnt actually prevent building and using models, its just a problem for stansummary. We do need to check it out. Hopefully I can replicate on one of the machines I have access too and its not a macos specific problem. |
Let me see if I can reproduce the issue on ubuntu, one sec. |
I can confirm that linux(ubuntu) builds fine. |
I take that back. That obviously works fine.
Thanks! Apart from figuring out the build issue (which is a cmdstan issue anyways), the other question is how to set up the arguments. There are at least the following options:
b)
c)
d)
The other thing is if we should separate the I do not think we actually need to separate MPI sampling in a separate function so I would go with either a) or c), slightly preferring c) because we do not have to deal with someone defining mpi_nprocess and -n in args. |
If we don't have a separate method @mitzimorris What do you think about these options? If this is going to be implemented in CmdStanPy too then we should make sure to coordinate on this. |
I'd pick a) because frequently # of procs is all one supplies for mpi runs. It'll make args slightly cleaner. |
There are the following options:
Cool. You definitely have waaay more experience running these so I am definitely going to trust your opinion here. Count me in for a) as well. |
Ok I'm also inclined to trust @yizhang-yiz's opinion since he has the most experience with MPI (I've never even tried using it!). @mitzimorris What do you think about the proposed function signature? |
And the argument names would then be |
What we need to put consideration is how this would interact with HPC task schedulers. Things like SLURM ask for the # of procs when a job is submitted, and this input should not conflict with what we put in a)(or c)). So maybe we can allow |
One reason I might slightly prefer a separate |
@jgabry has a point. Additional benefit in |
You mean like the user sets N but SLURM overrides that with M? Not sure if we can catch that in cmdstan(r). Apart from maybe running external commands?
Good point yeah. We can start with Thanks for the input @yizhang-yiz ! |
I don't think we can, which is why we'll need hand the decision to the user so they can choose which arg to provide to cmdstanr, and if |
I just made a few small edits. This seems ready (thanks @rok-cesnovar) so approving now. But @yizhang-yiz if you have time can you try it out one more time and see if the doc is missing anything important? |
Sorry guys I missed the thread yesterday. I can play with it later today. |
Thanks Yi! |
Works like a charm! Thank you! @rok-cesnovar @jgabry |
Thank you for the insight, discussion, and testing! Will then go ahead and merge. For now, this will be available in the Github version but we will most likely do a 0.2.3 or 0.3.0 release soon-ish. |
So I decided to try this on cross-chain warmup I was working on. library("cmdstanr")
cmdstan_make_local(cpp_options = list("MPI_ADAPTED_WARMUP" = "1", "TBB_CXX_TYPE"="clang"))
rebuild_cmdstan()
mod <- cmdstan_model("cmdstan/examples/eight_schools.stan", quiet=FALSE, force_recompile=TRUE)
f <- mod$sample_mpi(data = "cmdstan/examples/eight_schools/eight_schools.data.R", chains = 1, mpi_args = list("n" = 4), refresh = 200,output_dir="cmdstan/examples/eight_schools") Output: Chain 1 Iteration: 1 / 2000 [ 0%] (Warmup)
Chain 1 Iteration: 1 / 2000 [ 0%] (Warmup)
Chain 1 Iteration: 1 / 2000 [ 0%] (Warmup)
Chain 1 Iteration: 1 / 2000 [ 0%] (Warmup)
Chain 1 iteration: 100 window: 1 / 1 Rhat: 1.0289 ESS: 149.4547
Chain 1 cross-chain adaptation time: 0 seconds
Chain 1 Iteration: 200 / 2000 [ 10%] (Warmup)
Chain 1 Iteration: 200 / 2000 [ 10%] (Warmup)
Chain 1 Iteration: 200 / 2000 [ 10%] (Warmup)
Chain 1 Iteration: 200 / 2000 [ 10%] (Warmup)
Chain 1 iteration: 200 window: 1 / 2 Rhat: 1.0006 ESS: 373.0270
Chain 1 iteration: 200 window: 2 / 2 Rhat: 1.0002 ESS: 233.4405
Chain 1 cross-chain adaptation time: 0 seconds
Chain 1 Iteration: 1001 / 2000 [ 50%] (Sampling)
Chain 1 Iteration: 1001 / 2000 [ 50%] (Sampling)
Chain 1 Iteration: 1001 / 2000 [ 50%] (Sampling)
Chain 1 Iteration: 1001 / 2000 [ 50%] (Sampling)
Chain 1 Iteration: 1200 / 2000 [ 60%] (Sampling)
Chain 1 Iteration: 1200 / 2000 [ 60%] (Sampling)
Chain 1 Iteration: 1200 / 2000 [ 60%] (Sampling)
Chain 1 Iteration: 1200 / 2000 [ 60%] (Sampling)
Chain 1 Iteration: 1400 / 2000 [ 70%] (Sampling)
Chain 1 Iteration: 1400 / 2000 [ 70%] (Sampling)
Chain 1 Iteration: 1400 / 2000 [ 70%] (Sampling)
Chain 1 Iteration: 1400 / 2000 [ 70%] (Sampling)
Chain 1 Iteration: 1600 / 2000 [ 80%] (Sampling)
Chain 1 Iteration: 1600 / 2000 [ 80%] (Sampling)
Chain 1 Iteration: 1600 / 2000 [ 80%] (Sampling)
Chain 1 Iteration: 1600 / 2000 [ 80%] (Sampling)
Chain 1 Iteration: 1800 / 2000 [ 90%] (Sampling)
Chain 1 Iteration: 1800 / 2000 [ 90%] (Sampling)
Chain 1 Iteration: 1800 / 2000 [ 90%] (Sampling)
Chain 1 Iteration: 2000 / 2000 [100%] (Sampling)
Chain 1 finished in 0.0 seconds.
Error: Supplied CSV file is corrupt! The warmup algorithm works well, but looks like cmdstan's I/O is taken over by R, as a cmdstan run would return 4 CSV files from 4 communicating chains but in working directory there's only one empty CSV. Any idea what's going on? @rok-cesnovar |
Can you specify validate_csv = FALSE and see if the CSVs remain without the error. The CSVs might get deleted because of the error. Will take a look, one of the reasons I was so keen on getting this MPI in cmdstanr was testing this cross-chain warmup: |
"validate_csv=false" doesn't help. The output remains to be a single empty CSV files while I expect 4 files, one for each chain. The easiest way to access this experimental feature is to use Torsten repo: and set cmdstan path to Torsten/cmdstan. |
Ok, thanks, will take a look now. How do the files differ? By suffix? Say I give |
I don't see anything different: > f <- mod$sample_mpi(data = "cmdstan/examples/eight_schools/eight_schools.data.R", chains = 1, mpi_args = list("n" = 4), refresh = 200,output_dir="cmdstan/examples/eight_schools",validate_csv=FALSE)
Running MCMC with 1 chain...
...
Chain 1 Iteration: 1800 / 2000 [ 90%] (Sampling)
Chain 1 Iteration: 2000 / 2000 [100%] (Sampling)
Chain 1 finished in 0.0 seconds.
> f$output_files()
[1] "/Users/yiz/Work/Torsten/cmdstan/examples/eight_schools/eight_schools-202012080911-1-818735.csv"
> f <- mod$sample_mpi(data = "cmdstan/examples/eight_schools/eight_schools.data.R", chains = 1, mpi_args = list("n" = 4), refresh = 200,output_dir="cmdstan/examples/eight_schools")
Running MCMC with 1 chain...
...
Chain 1 Iteration: 2000 / 2000 [100%] (Sampling)
Chain 1 finished in 0.1 seconds.
Error: Supplied CSV file is corrupt!
> f$output_files()
[1] "/Users/yiz/Work/Torsten/cmdstan/examples/eight_schools/eight_schools-202012080911-1-818735.csv" While in cmdstan I'm getting bash-3.2$ make -j4 examples/eight_schools/eight_schools && cd examples/eight_schools/
bash-3.2$ mpiexec -n 4 -l ./eight_schools sample data file=eight_schools.data.R
...
[0] Elapsed Time: 0.016 seconds (Warm-up)
[0] 0.07 seconds (Sampling)
[0] 0.086 seconds (Total)
[0]
bash-3.2$ ls *.csv
mpi.0.output.csv mpi.1.output.csv mpi.2.output.csv mpi.3.output.csv
|
Thanks, I was asking for |
This is the command that is used
Running this in the command line produces the same thing (1 CSV). |
You can install remotes::install_github("stan-dev/cmdstanr@echomd") that will print the command. |
Great! Thanks. Then that's likely caused by bugs in my code. |
I am not so sure just yet. |
I can confirm replacing output file in the above command with bash-3.2$ mpiexec -n 4 ./eight_schools 'id=1' random 'seed=208921277' data 'file=eight_schools.data.R' output 'file=eight_schools-202012081831-1-6f6627.csv' 'refresh=200' 'method=sample' 'save_warmup=0' 'algorithm=hmc' 'engine=nuts' adapt 'engaged=1' &> out.log
bash-3.2$ ls *.csv
mpi.0.eight_schools-202012081831-1-6f6627.csv mpi.2.eight_schools-202012081831-1-6f6627.csv
mpi.1.eight_schools-202012081831-1-6f6627.csv mpi.3.eight_schools-202012081831-1-6f6627.csv so I must have messed up the ostream path. |
I would say this is the culprit yes if the file is specified with absolute paths:
|
Just fixed it, now it works f <- mod$sample_mpi(data = "cmdstan/examples/eight_schools/eight_schools.data.R", chains = 1, mpi_args = list("n" = 4), refresh = 200,output_dir="cmdstan/examples/eight_schools",validate_csv=FALSE)
Running MCMC with 1 chain...
Chain 1 stepsize_jitter = 0 (Default)
Chain 1 id = 1
Chain 1 data
Chain 1 file = /Users/yiz/Work/cmdstan/examples/eight_schools/eight_schools.data.R
Chain 1 init = 2 (Default)
Chain 1 random
Chain 1 seed = 604574839
Chain 1 output
Chain 1 file = /Users/yiz/Work/cmdstan/examples/eight_schools/eight_schools-202012081145-1-3326b5.mpi.1.csv
Chain 1 diagnostic_file = (Default)
Chain 1 refresh = 200
Chain 1 sig_figs = -1 (Default)
Chain 1 num_cross_chains = 4 (Default)
Chain 1 cross_chain_window = 100 (Default)
Chain 1 cross_chain_rhat = 1.05 (Default)
Chain 1 cross_chain_ess = 200 (Default)
Chain 1 algorithm = hmc (Default)
Chain 1 hmc
Chain 1 engine = nuts (Default)
Chain 1 nuts
Chain 1 max_depth = 10 (Default)
Chain 1 metric = diag_e (Default)
Chain 1 metric_file = (Default)
Chain 1 stepsize = 1 (Default)
Chain 1 stepsize_jitter = 0 (Default)
Chain 1 id = 2
Chain 1 data
Chain 1 file = /Users/yiz/Work/cmdstan/examples/eight_schools/eight_schools.data.R
Chain 1 init = 2 (Default)
Chain 1 random
Chain 1 seed = 604574839
Chain 1 output
Chain 1 file = /Users/yiz/Work/cmdstan/examples/eight_schools/eight_schools-202012081145-1-3326b5.mpi.2.csv
Chain 1 diagnostic_file = (Default)
Chain 1 refresh = 200
Chain 1 sig_figs = -1 (Default)
Chain 1 t0 = 10 (Default)
Chain 1 init_buffer = 75 (Default)
Chain 1 term_buffer = 50 (Default)
Chain 1 window = 25 (Default)
Chain 1 num_cross_chains = 4 (Default)
Chain 1 cross_chain_window = 100 (Default)
Chain 1 cross_chain_rhat = 1.05 (Default)
Chain 1 cross_chain_ess = 200 (Default)
Chain 1 algorithm = hmc (Default)
Chain 1 hmc
Chain 1 engine = nuts (Default)
Chain 1 nuts
Chain 1 max_depth = 10 (Default)
Chain 1 metric = diag_e (Default)
Chain 1 metric_file = (Default)
Chain 1 stepsize = 1 (Default)
Chain 1 stepsize_jitter = 0 (Default)
Chain 1 id = 3
Chain 1 data
Chain 1 file = /Users/yiz/Work/cmdstan/examples/eight_schools/eight_schools.data.R
Chain 1 init = 2 (Default)
Chain 1 random
Chain 1 seed = 604574839
Chain 1 output
Chain 1 file = /Users/yiz/Work/cmdstan/examples/eight_schools/eight_schools-202012081145-1-3326b5.mpi.3.csv
Chain 1 diagnostic_file = (Default)
Chain 1 refresh = 200
Chain 1 sig_figs = -1 (Default)
Chain 1 Iteration: 1 / 2000 [ 0%] (Warmup)
Chain 1 Iteration: 1 / 2000 [ 0%] (Warmup)
Chain 1 Iteration: 1 / 2000 [ 0%] (Warmup)
Chain 1 iteration: 100 window: 1 / 1 Rhat: 1.0212 ESS: 79.6405
Chain 1 cross-chain adaptation time: 0 seconds
Chain 1 Iteration: 200 / 2000 [ 10%] (Warmup)
Chain 1 Iteration: 200 / 2000 [ 10%] (Warmup)
Chain 1 Iteration: 200 / 2000 [ 10%] (Warmup)
Chain 1 Iteration: 200 / 2000 [ 10%] (Warmup)
Chain 1 iteration: 200 window: 1 / 2 Rhat: 1.0181 ESS: 170.9020
Chain 1 iteration: 200 window: 2 / 2 Rhat: 1.0141 ESS: 135.0918
Chain 1 cross-chain adaptation time: 0 seconds
Chain 1 iteration: 300 window: 1 / 3 Rhat: 1.0104 ESS: 310.1229
Chain 1 iteration: 300 window: 2 / 3 Rhat: 1.0052 ESS: 275.0363
Chain 1 iteration: 300 window: 3 / 3 Rhat: 1.0006 ESS: 144.5106
Chain 1 cross-chain adaptation time: 0 seconds
Chain 1 Iteration: 1001 / 2000 [ 50%] (Sampling)
Chain 1 Iteration: 1001 / 2000 [ 50%] (Sampling)
...
Chain 1 finished in 0.1 seconds. Though there's still a dummy csv file generated and pointed to by output_files(): f$output_files()
[1] "/Users/yiz/Work/cmdstan/examples/eight_schools/eight_schools-202012081145-1-3326b5.csv" In addition, what's the best way to add custom options to mpiexec -n 4 ./eight_schools sample adapt cross_chain_ess=400 data file... Here |
Yay!
Hm, that would probably be because of https://github.com/stan-dev/cmdstanr/blob/master/R/args.R#L99
See commit dbee414 and just duplicate for other args :) |
Summary
A draft for MPI sampling. Still details to hash out, just wanted to get a version out.
Files from: https://github.com/rmcelreath/cmdstan_map_rect_tutorial
@yizhang-yiz sorry this took so long. Whenever you have time and if you are still interested, would you try it out and give your thoughts?
Use
to install this version.
Copyright and Licensing
Please list the copyright holder for the work you are submitting
(this will be you or your assignee, such as a university or company):
Rok Češnovar, Uni. of Ljubljana
Yi Zhang (initial version for testing)
By submitting this pull request, the copyright holder is agreeing to
license the submitted work under the following licenses: