SymEngine and code generation - isuruf/symengine GitHub Wiki

This blog post will cover two recent features of SymEngine and SymPy

  1. Using SymEngine as the symbolic backend for sympy.physics.mechanics
  2. Generating LLVM IR for SymEngine expressions

SymEngine is a C++ library with Python wrappers which hopes to replace SymPy's core in the future. SymPy's core itself is huge and there are many things in SymPy's core (sympy.core, sympy.functions) that need to ported over to SymEngine. As the first step of this transition, we made a proof of concept by using sympy.physics.mechanics. Reason for using the mechanics submodule was that it was small, doesn't have any dependees and the most important of all, uses a very small subset of SymPy. Most of this subset of SymPy have been ported over to SymEngine and therefore we can try using SymEngine.

With the latest SymPy master, you can use SymEngine as the backend for the mechanics submodule. Mechanics submodules doesn't import sympy objects by simply doing from sympy import Symbol. It imports them by doing, from sympy.core.backend import Symbol. Symbol from sympy.core.backend will be either sympy.core.symbol.Symbol or symengine.lib.symengine_wrapper.Symbol depending on the value of an environment variable USE_SYMENGINE. Set it to 1 to use symengine. With this change, the mechanics module works and all tests in mechanics module passes. Benchmarks suggest that using SymEngine has a huge boost in speedup.

Following timings are for a benchmark from SymPy called n_link_pendulum_on_cart

n SymEngine + SymPy SymPy only Speedup
10 0.103 s 4.145 s 40.24x
15 0.265 s 14.827 s 55.95x
20 0.541 s 36.166 s 66.85x
30 1.596 s 131.874 s 82.62x
40 3.52 s 347.113 s 98.61x
50 6.73 s 756.875 s 112.46x
60 11.56 s 1671.174 s 144.56x

Now that we can use SymEngine in SymPy's mechanics module, let's use it on PyDy. PyDy, short for Python Dynamics, is a tool kit written in Python to enable the study of multibody dynamics. It uses SymPy for generating equations of motion of a dynamic system symbolically and then use the equations for simulating the system. For simulation, ODEs representing the system have to be numerically integrated and therefore requires multiple evaluations of the equations with numeric values.

That's where code generation comes in. Substituting numerical values for symbols in the expression tree and reevaluating the tree is very slow when the expressions are large. Therefore, the expression is converted to an equivalent form (it can be a compiled C function or a Python lambda function. etc.) that can be used for fast numeric evaluation. This step of conversion will be called code generation. PyDy has following code generators,

  1. LambdifyGenerator - Uses SymPy's lambdify to output Python code for converting an expression to a Python lambda function
  2. TheanoGenerator - Uses Theano to convert an expression to a compiled C function
  3. CythonGenerator - Uses SymPy to output C code equivalent to an expression and uses Cython to compile it and use from Python

These generators have different benefits. When you want to do fast prototyping, LambdifyGenerator is useful as code generation is fastest, but numerical evaluation is the slowest among the three. CythonGenerator is the fastest when it comes to numerical evaluation, but is very slow in code generation compared to LambdifyGenerator. This is useful for actual use, but too slow for any use in prototyping. See this blog post for more details.

SymEngine now supports code generation as well. One functionality we are going to look at is lambdify with llvm backend. LLVM is a compiler infrastructure that enables writing compilers. LLVM is the backend used by clang which converts C/C++ code to an intermediate representation called LLVM IR. LLVM then does optimizations on it and compiles it to native code. SymEngine directly generates LLVM IR and then uses LLVM to compile it to native code and run it. Comparing this to CythonGenerator, this avoids several steps. CythonGenerator goes through the expression tree, generate a string of C code, then uses a compiler to compile it. This compiler would parse the C code and transform it to an intermediate representation which is LLVM IR if you are using clang. SymEngine avoids all of this redundancy and the steps after that are basically the same for both CythonGenerator and SymEngine's lambdify.

Therefore, we would expect a SymEngine's lambdify to be as fast as CythonGenerator in the integration step and much faster for the code generation step. SymEngine's lambdify's code generation step was faster than even LambdifyGenerator of SymPy which was a nice surprise.

Here are some results from a benchmark of PyDy. This creates equations of motions for a n link pendulum of cart and use SciPy for integrating the ODEs by providing callbacks created by the code generators.

To try it out, use the latest master from symengine and compile with -DINTEGER_CLASS=flint -DWITH_LLVM=yes. You can get flint from conda by doing, conda install -c conda-forge libflint. To install LLVM, conda install -c conda-forge llvmdev=3.8 (llvm-3.8-dev package from ubuntu is broken). Make sure to use gcc < 5 if you are using conda's llvmdev or do export CXXFLAGS="-D_GLIBCXX_USE_CXX11_ABI=0" before calling cmake. You'll also need the latest master of symengine.py, sympy and PyDy PR#360.