AOT Code Generation¶
anvil generates standalone C++ code from NumericalFunctions. The generated files have no Python dependency and can be compiled into any C++ project.
Generating a module¶
This produces my_module.hpp and my_module.cpp.
Module structure¶
Each module is wrapped in a namespace matching the module name. Each function gets its own nested namespace:
namespace my_module {
constexpr int dim = 1024; // codegen constants (if any)
namespace my_function {
// function-specific types and declarations
}
namespace my_function_jac {
// sparse derivative metadata + types
}
} // namespace my_module
Buffer struct¶
The Buffer<T, Ns...> template is a minimal wrapper around a raw C pointer that provides compile-time known shape and SIMD-compatible memory alignment:
template <typename T, std::size_t... Ns> struct Buffer {
T *data;
static constexpr std::size_t alignment = 16; // NEON-compatible
static constexpr std::size_t ndim = sizeof...(Ns);
static constexpr std::array<std::size_t, ndim> shape = {Ns...};
static constexpr std::size_t size = (Ns * ... * 1);
static constexpr bool is_scalar = ndim == 0;
static constexpr std::size_t nbytes = size * sizeof(T);
static Buffer alloc(); // Allocate aligned memory
static void free(Buffer &buffer); // Free memory
};
Scalars use Buffer<double> (empty shape):
Function interface¶
Each function exposes typed buffer aliases and a call function:
namespace my_function {
typedef Buffer<double, 1024> IN0_t; // Input buffer type
typedef Buffer<double, 1024> OUT0_t; // Output buffer type
typedef Buffer<signed char, 4096> WS_t; // Workspace type
WS_t init_ws(); // Initialize workspace (call once)
void call(const IN0_t& in0, const OUT0_t& out0, const WS_t& ws);
}
Typical usage:
auto in = my_function::IN0_t::alloc();
auto out = my_function::OUT0_t::alloc();
auto ws = my_function::init_ws();
// Fill input
Eigen::Map<Eigen::VectorXd>(in.data, in.size).setRandom();
// Call
my_function::call(in, out, ws);
// Clean up
my_function::IN0_t::free(in);
my_function::OUT0_t::free(out);
my_function::WS_t::free(ws);
Workspace¶
The workspace (WS_t) contains:
- Intermediate buffers: memory for temporary values computed between kernels
- Constant buffers: pre-copied constant data (initialized in
init_ws())
The workspace size is determined at codegen time. For functions with no intermediate or constant buffers, WS_t has size 0.
init_ws() allocates the workspace and copies constant data:
WS_t init_ws() {
auto ws = WS_t::alloc();
// Copy embedded constants into workspace slots
std::memcpy(intermediate0.data, constant0, sizeof(constant0));
return ws;
}
Constant buffers¶
Constants detected during tracing (matrices, lookup tables, etc.) are embedded as static constexpr arrays in the generated source:
These are copied into the workspace during init_ws().
Codegen constants¶
Use CodegenIntConstant to declare named integer constants in the generated header:
import anvil as av
dim = 1024
av.generate_module("my_module", [fn], constants={av.CodegenIntConstant("dim", dim)})
Generated header:
CodegenIntConstant supports arithmetic -- you can build expressions that track dependencies:
N = av.CodegenIntConstant("N", 10)
nz = av.CodegenIntConstant("nz", 6)
total = N * nz # CodegenIntConstant("N * nz", 60), depends on N and nz
When using SQP solvers, dimension constants (n, nparam, m_eq, m_ineq) are automatically added to the generated header, prefixed with the solver name.