From 14b8c1288108907036b362a076133e017229ca40 Mon Sep 17 00:00:00 2001 From: amas Date: Fri, 14 Nov 2025 11:05:12 -0500 Subject: [PATCH 1/9] Add multithreading and stanc_optimizations to CmdStanModel --- cmdstanpy/model.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/cmdstanpy/model.py b/cmdstanpy/model.py index 5cea19f2..6f2876ca 100644 --- a/cmdstanpy/model.py +++ b/cmdstanpy/model.py @@ -93,6 +93,9 @@ def __init__( stanc_options: dict[str, Any] | None = None, cpp_options: dict[str, Any] | None = None, user_header: OptionalPath = None, + *, + multithreading: bool = False, + stanc_optimizations: bool = False, ) -> None: """ Initialize object given constructor args. @@ -105,10 +108,19 @@ def __init__( :param cpp_options: Options for C++ compiler. :param user_header: A path to a header file to include during C++ compilation. + :param multithreading: Enables multithreading in a Stan model. + Equivalent to `cpp_options = {"STAN_THREADS": "TRUE"}`. + Defaults to False. + :param stanc_optimizations: Enables O1 optimizations in the + stanc compiler. Equivalent to `stanc_options = {"O": 1}`. + Defaults to False. """ self._name = '' self._stan_file = None - self._stanc_options: dict[str, Any] = stanc_options or {} + self._stanc_options = self._resolve_stanc_options( + stanc_options, stanc_optimizations + ) + cpp_options = self._resolve_cpp_options(cpp_options, multithreading) self._fixed_param = False @@ -246,6 +258,24 @@ def code(self) -> str | None: ) return code + @staticmethod + def _resolve_cpp_options( + cpp_options: dict[str, Any] | None, multithreading: bool + ) -> dict[str, Any]: + out = {} + if multithreading: + out["STAN_THREADS"] = "TRUE" + return out | cpp_options if cpp_options else out + + @staticmethod + def _resolve_stanc_options( + stanc_options: dict[str, Any] | None, stanc_optimizations: bool + ) -> dict[str, Any]: + out = {} + if stanc_optimizations: + out["O"] = 1 + return out | stanc_options if stanc_options else out + def optimize( self, data: Mapping[str, Any] | str | os.PathLike | None = None, From 59ecc82af6c0443cca3ffcb468ed0a7374eb7aad Mon Sep 17 00:00:00 2001 From: amas Date: Fri, 14 Nov 2025 11:13:19 -0500 Subject: [PATCH 2/9] Test compilation options resolution --- test/test_model.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/test_model.py b/test/test_model.py index 8334560a..f4149f10 100644 --- a/test/test_model.py +++ b/test/test_model.py @@ -471,3 +471,29 @@ def test_diagnose() -> None: require_gradients_ok=False, ) assert np.abs(gradients["error"]).max() > 1e-3 + + +def test_compilation_options_resolution() -> None: + model = CmdStanModel(stan_file=BERN_STAN) + + out = model._resolve_cpp_options(None, multithreading=False) + assert not out + out = model._resolve_cpp_options(None, multithreading=True) + assert out == {"STAN_THREADS": "TRUE"} + out = model._resolve_cpp_options({"STAN_THREADS": ""}, multithreading=True) + assert out == {"STAN_THREADS": ""} + out = model._resolve_cpp_options( + {"STAN_OPENCL": "TRUE"}, multithreading=True + ) + assert out == {"STAN_THREADS": "TRUE", "STAN_OPENCL": "TRUE"} + + out = model._resolve_stanc_options(None, stanc_optimizations=False) + assert not out + out = model._resolve_stanc_options(None, stanc_optimizations=True) + assert out == {"O": 1} + out = model._resolve_stanc_options({"O": 0}, stanc_optimizations=True) + assert out == {"O": 0} + out = model._resolve_stanc_options( + {"O": "experimental"}, stanc_optimizations=True + ) + assert out == {"O": "experimental"} From 889bd8ab5a42e70b044e0a32267c656d293571fa Mon Sep 17 00:00:00 2001 From: amas Date: Fri, 14 Nov 2025 12:24:54 -0500 Subject: [PATCH 3/9] Clarify compilation options docstring --- cmdstanpy/model.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cmdstanpy/model.py b/cmdstanpy/model.py index 6f2876ca..660ce97a 100644 --- a/cmdstanpy/model.py +++ b/cmdstanpy/model.py @@ -104,8 +104,10 @@ def __init__( :param exe_file: Path to compiled executable file. :param force_compile: Whether or not to force recompilation if executable file already exists. - :param stanc_options: Options for stanc compiler. - :param cpp_options: Options for C++ compiler. + :param stanc_options: Options for stanc compiler. Note, this + will override the `stanc_optimizations` if in conflict. + :param cpp_options: Options for C++ compiler. Note, this will + override the `multithreading` option if in conflict. :param user_header: A path to a header file to include during C++ compilation. :param multithreading: Enables multithreading in a Stan model. From 3c0d76461bffb01dc599aa2ca1a1f886b87991d5 Mon Sep 17 00:00:00 2001 From: amas Date: Fri, 14 Nov 2025 12:25:24 -0500 Subject: [PATCH 4/9] Update compilation section of workflow --- docsrc/users-guide/workflow.rst | 55 +++++++++++++++------------------ 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/docsrc/users-guide/workflow.rst b/docsrc/users-guide/workflow.rst index 82bafc46..453301a1 100644 --- a/docsrc/users-guide/workflow.rst +++ b/docsrc/users-guide/workflow.rst @@ -39,21 +39,14 @@ managing the resulting inference for a single model and set of inputs. Compile the Stan model ^^^^^^^^^^^^^^^^^^^^^^ -The: :class:`CmdStanModel` class provides methods -to compile and run the Stan program. -A CmdStanModel object can be instantiated by specifying -either a Stan file or the executable file, or both. -If only the Stan file path is specified, the constructor will -check for the existence of a correspondingly named exe file in -the same directory. If found, it will use this as the exe file path. - -By default, when a CmdStanModel object is instantiated from a Stan file, -the constructor will compile the model as needed. -The constructor argument `compile` controls this behavior. - -* ``compile=False``: never compile the Stan file. -* ``compile="Force"``: always compile the Stan file. -* ``compile=True``: (default) compile the Stan file as needed, i.e., if no exe file exists or if the Stan file is newer than the exe file. +The :class:`CmdStanModel` class provides methods to compile and run the Stan +program. A CmdStanModel object can be instantiated by specifying a Stan file, +the executable file, or both. If only the Stan file path is specified, the +constructor will check for the existence of a correspondingly named executable in +the same directory. If found, it will use this as the exe file path. + +When a CmdStanModel object is instantiated from a Stan file, the constructor +will compile the model if the executable is non-existent or out-of-date. .. code-block:: python @@ -67,8 +60,8 @@ The constructor argument `compile` controls this behavior. my_model.exe_file my_model.code() -The CmdStanModel class also provides the :meth:`~CmdStanModel.compile` method, -which can be called at any point to (re)compile the model as needed. +The ``force_compile=True`` argument can be passed to the CmdStanModel +constructor, which will force (re)compilation of the model. Model compilation is carried out via the GNU Make build tool. The CmdStan ``makefile`` contains a set of general rules which @@ -83,20 +76,22 @@ Model compilation is done in two steps: * The C++ compiler compiles the generated code and links in the necessary supporting libraries. -Therefore, both the constructor and the ``compile`` method -allow optional arguments ``stanc_options`` and ``cpp_options`` which -specify options for each compilation step. -Options are specified as a Python dictionary mapping -compiler option names to appropriate values. +The constructor accepts arguments to specify both ``stanc`` and C++ compilation +options, if desired. Passing `multithreading=True` enables the **STAN_THREADS** +C++ flag, which is needed to parallelize within-chain computations, such as +with ``reduce_sum``, or to parallelize the NUTS-HMC sampler across chains. +Passing ``stanc_optimizations=True`` will enable ``O1`` optimizations in the +``stanc`` compiler. -In order parallelize within-chain computations using the -Stan language ``reduce_sum`` function, or to parallelize -running the NUTS-HMC sampler across chains, -the Stan model must be compiled with -C++ compiler flag **STAN_THREADS**. -While any value can be used, -we recommend the value ``True``, e.g.: +Outside of these common options, the constructor accepts the optional arguments +``stanc_options`` and ``cpp_options``, which allow specifying arbitrary +compilation options. Some more advanced Stan features, like MPI or OpenCL +support, require using these. Note that if the lower-level compilation options +conflict with an argument like ``multithreading=True``, the option in +``stanc_options`` or ``cpp_options`` takes precedence. +An example model compilation that enables multithreading and +basic optimization can be done like so: .. code-block:: python @@ -104,7 +99,7 @@ we recommend the value ``True``, e.g.: from cmdstanpy import CmdStanModel my_stanfile = os.path.join('.', 'my_model.stan') - my_model = CmdStanModel(stan_file=my_stanfile, cpp_options={'STAN_THREADS':'true'}) + my_model = CmdStanModel(stan_file=my_stanfile, multithreading=True, stanc_optimizations=True) Assemble input and initialization data From 9b1759ea36212ea87fe00a0271714706bf149b5b Mon Sep 17 00:00:00 2001 From: amas Date: Fri, 14 Nov 2025 13:25:57 -0500 Subject: [PATCH 5/9] Update broken import in documentation notebook --- docsrc/users-guide/examples/Pathfinder.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docsrc/users-guide/examples/Pathfinder.ipynb b/docsrc/users-guide/examples/Pathfinder.ipynb index e5f3c564..4a0ed002 100644 --- a/docsrc/users-guide/examples/Pathfinder.ipynb +++ b/docsrc/users-guide/examples/Pathfinder.ipynb @@ -44,7 +44,7 @@ "outputs": [], "source": [ "import os\n", - "from cmdstanpy.model import CmdStanModel, cmdstan_path" + "from cmdstanpy import CmdStanModel, cmdstan_path" ] }, { From 9dc511198edc28da858e05b7d3c28c62779078e1 Mon Sep 17 00:00:00 2001 From: amas Date: Fri, 14 Nov 2025 15:17:23 -0500 Subject: [PATCH 6/9] Clarify resolve stanc/cpp options functions --- cmdstanpy/model.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/cmdstanpy/model.py b/cmdstanpy/model.py index 660ce97a..61586d51 100644 --- a/cmdstanpy/model.py +++ b/cmdstanpy/model.py @@ -264,19 +264,21 @@ def code(self) -> str | None: def _resolve_cpp_options( cpp_options: dict[str, Any] | None, multithreading: bool ) -> dict[str, Any]: - out = {} - if multithreading: + out = cpp_options or {} + out = out.copy() + if multithreading and "STAN_THREADS" not in out: out["STAN_THREADS"] = "TRUE" - return out | cpp_options if cpp_options else out + return out @staticmethod def _resolve_stanc_options( stanc_options: dict[str, Any] | None, stanc_optimizations: bool ) -> dict[str, Any]: - out = {} - if stanc_optimizations: + out = stanc_options or {} + out = out.copy() + if stanc_optimizations and "O" not in out: out["O"] = 1 - return out | stanc_options if stanc_options else out + return out def optimize( self, From b77db20e181daeb72c97175b70a10cd4ee13a5c8 Mon Sep 17 00:00:00 2001 From: amas Date: Fri, 14 Nov 2025 15:20:35 -0500 Subject: [PATCH 7/9] Move resolve_*_options to compilation --- cmdstanpy/compilation.py | 20 ++++++++++++++++++++ cmdstanpy/model.py | 26 ++++---------------------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/cmdstanpy/compilation.py b/cmdstanpy/compilation.py index 54e099fe..afc75812 100644 --- a/cmdstanpy/compilation.py +++ b/cmdstanpy/compilation.py @@ -486,3 +486,23 @@ def format_stan_file( except (ValueError, RuntimeError) as e: raise RuntimeError("Stanc formatting failed") from e + + +def resolve_cpp_options( + cpp_options: dict[str, Any] | None, multithreading: bool +) -> dict[str, Any]: + out = cpp_options or {} + out = out.copy() + if multithreading and "STAN_THREADS" not in out: + out["STAN_THREADS"] = "TRUE" + return out + + +def resolve_stanc_options( + stanc_options: dict[str, Any] | None, stanc_optimizations: bool +) -> dict[str, Any]: + out = stanc_options or {} + out = out.copy() + if stanc_optimizations and "O" not in out: + out["O"] = 1 + return out diff --git a/cmdstanpy/model.py b/cmdstanpy/model.py index 61586d51..80927a63 100644 --- a/cmdstanpy/model.py +++ b/cmdstanpy/model.py @@ -119,10 +119,12 @@ def __init__( """ self._name = '' self._stan_file = None - self._stanc_options = self._resolve_stanc_options( + self._stanc_options = compilation.resolve_stanc_options( stanc_options, stanc_optimizations ) - cpp_options = self._resolve_cpp_options(cpp_options, multithreading) + cpp_options = compilation.resolve_cpp_options( + cpp_options, multithreading + ) self._fixed_param = False @@ -260,26 +262,6 @@ def code(self) -> str | None: ) return code - @staticmethod - def _resolve_cpp_options( - cpp_options: dict[str, Any] | None, multithreading: bool - ) -> dict[str, Any]: - out = cpp_options or {} - out = out.copy() - if multithreading and "STAN_THREADS" not in out: - out["STAN_THREADS"] = "TRUE" - return out - - @staticmethod - def _resolve_stanc_options( - stanc_options: dict[str, Any] | None, stanc_optimizations: bool - ) -> dict[str, Any]: - out = stanc_options or {} - out = out.copy() - if stanc_optimizations and "O" not in out: - out["O"] = 1 - return out - def optimize( self, data: Mapping[str, Any] | str | os.PathLike | None = None, From 4429e464d33cd02bb11787a7d39af2967b0ddfd6 Mon Sep 17 00:00:00 2001 From: amas Date: Fri, 14 Nov 2025 15:21:23 -0500 Subject: [PATCH 8/9] Move new compilation flags higher in documentation --- cmdstanpy/model.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cmdstanpy/model.py b/cmdstanpy/model.py index 80927a63..ece9fd2e 100644 --- a/cmdstanpy/model.py +++ b/cmdstanpy/model.py @@ -104,18 +104,18 @@ def __init__( :param exe_file: Path to compiled executable file. :param force_compile: Whether or not to force recompilation if executable file already exists. - :param stanc_options: Options for stanc compiler. Note, this - will override the `stanc_optimizations` if in conflict. - :param cpp_options: Options for C++ compiler. Note, this will - override the `multithreading` option if in conflict. - :param user_header: A path to a header file to include during C++ - compilation. :param multithreading: Enables multithreading in a Stan model. Equivalent to `cpp_options = {"STAN_THREADS": "TRUE"}`. Defaults to False. :param stanc_optimizations: Enables O1 optimizations in the stanc compiler. Equivalent to `stanc_options = {"O": 1}`. Defaults to False. + :param stanc_options: Options for stanc compiler. Note, this + will override the `stanc_optimizations` if in conflict. + :param cpp_options: Options for C++ compiler. Note, this will + override the `multithreading` option if in conflict. + :param user_header: A path to a header file to include during C++ + compilation. """ self._name = '' self._stan_file = None From 64f0f1b86416a291ff41b76fcd86cad78ecd8594 Mon Sep 17 00:00:00 2001 From: amas Date: Fri, 14 Nov 2025 15:30:46 -0500 Subject: [PATCH 9/9] Move unit test --- test/test_compilation.py | 27 ++++++++++++++++++++++++++- test/test_model.py | 26 -------------------------- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/test/test_compilation.py b/test/test_compilation.py index 731a5c8b..addaea55 100644 --- a/test/test_compilation.py +++ b/test/test_compilation.py @@ -9,7 +9,12 @@ import pytest -from cmdstanpy.compilation import CompilerOptions, format_stan_file +from cmdstanpy.compilation import ( + CompilerOptions, + format_stan_file, + resolve_cpp_options, + resolve_stanc_options, +) HERE = os.path.dirname(os.path.abspath(__file__)) DATAFILES_PATH = os.path.join(HERE, 'data') @@ -225,3 +230,23 @@ def test_model_format_options() -> None: formatted = sys_stdout.getvalue() assert formatted.count('{') == 3 assert formatted.count('(') == 1 + + +def test_compilation_options_resolution() -> None: + out = resolve_cpp_options(None, multithreading=False) + assert not out + out = resolve_cpp_options(None, multithreading=True) + assert out == {"STAN_THREADS": "TRUE"} + out = resolve_cpp_options({"STAN_THREADS": ""}, multithreading=True) + assert out == {"STAN_THREADS": ""} + out = resolve_cpp_options({"STAN_OPENCL": "TRUE"}, multithreading=True) + assert out == {"STAN_THREADS": "TRUE", "STAN_OPENCL": "TRUE"} + + out = resolve_stanc_options(None, stanc_optimizations=False) + assert not out + out = resolve_stanc_options(None, stanc_optimizations=True) + assert out == {"O": 1} + out = resolve_stanc_options({"O": 0}, stanc_optimizations=True) + assert out == {"O": 0} + out = resolve_stanc_options({"O": "experimental"}, stanc_optimizations=True) + assert out == {"O": "experimental"} diff --git a/test/test_model.py b/test/test_model.py index f4149f10..8334560a 100644 --- a/test/test_model.py +++ b/test/test_model.py @@ -471,29 +471,3 @@ def test_diagnose() -> None: require_gradients_ok=False, ) assert np.abs(gradients["error"]).max() > 1e-3 - - -def test_compilation_options_resolution() -> None: - model = CmdStanModel(stan_file=BERN_STAN) - - out = model._resolve_cpp_options(None, multithreading=False) - assert not out - out = model._resolve_cpp_options(None, multithreading=True) - assert out == {"STAN_THREADS": "TRUE"} - out = model._resolve_cpp_options({"STAN_THREADS": ""}, multithreading=True) - assert out == {"STAN_THREADS": ""} - out = model._resolve_cpp_options( - {"STAN_OPENCL": "TRUE"}, multithreading=True - ) - assert out == {"STAN_THREADS": "TRUE", "STAN_OPENCL": "TRUE"} - - out = model._resolve_stanc_options(None, stanc_optimizations=False) - assert not out - out = model._resolve_stanc_options(None, stanc_optimizations=True) - assert out == {"O": 1} - out = model._resolve_stanc_options({"O": 0}, stanc_optimizations=True) - assert out == {"O": 0} - out = model._resolve_stanc_options( - {"O": "experimental"}, stanc_optimizations=True - ) - assert out == {"O": "experimental"}