Devito

repository·main·Indexed 20 days ago

https://github.com/devitocodes/devito

A Python-based framework and Finite Difference DSL for symbolic computation. Devito automates the generation of optimized code for high-performance stencil computations across CPUs, GPUs, and distributed clusters. It includes tools for performance benchmarking, auto-tuning, and profiling with Intel Advisor, as well as support for OpenMP and MPI parallel execution.

Tokens
26.7K
Snippets
83
Records
113
Agent score
72%

What's inside devito

  1. Overview of Devito core features

    main

    Devito is a Python package for optimized stencil computation (e.g., finite differences, image processing, machine learning) using symbolic specification.

    Key capabilities include:

    • Automated Code Generation: Generates optimized kernels for CPUs (SIMD, OpenMP), GPUs (OpenACC), and multi-node clusters (MPI).
    • Symbolic Math: Built on SymPy for high-level problem definitions.
    • Advanced Operators: Support for sparse operators (interpolation), linear operators (convolutions), tensor contractions, and boundary conditions.
    • Optimization: Includes an autotuning framework, blocking, and aggressive symbolic transformations for FLOP reduction.
    • Integration: Works with NumPy, SymPy, Dask, SciPy, TensorFlow, and PyTorch.
  2. Explore Devito tutorials and examples by domain

    main

    Devito provides categorized examples ranging from introductory symbolic computation to advanced seismic modeling and performance optimization. Use the following directory structure to find relevant learning paths:

    Beginner Paths

    • userapi: A gentle introduction to symbolic computation using the Devito API.
    • cfd: Introductory notebooks for implementing finite difference operators for Computational Fluid Dynamics (CFD), based on the "CFD Python: 12 steps to Navier-Stokes" curriculum.

    Advanced Seismic Modeling

    • seismic/tutorials: Incremental complexity notebooks covering custom stencils, staggered grids, tensor notation, and time blocking.
    • seismic/acoustic: Isotropic acoustic forward, adjoint, gradient, and Born operators for Full-Waveform Inversion (FWI).
    • seismic/tti: Anisotropic acoustic forward operators (Tilted Transverse Isotropy).
    • seismic/elastic: Isotropic elastic forward operators utilizing Devito's tensorial symbolic language.
    • seismic/viscoelastic: Isotropic viscoelastic forward operators using tensor functions for PDE discretization.
    • seismic/self-adjoint: Implementation of nonlinear forward and linearized Jacobian operators for energy-conserving pseudo-acoustic modeling.

    Specialized Domains & Features

    • mpi: Explanations and examples of how MPI (Message Passing Interface) works within Devito.
    • finance: Application of PDEs to financial modeling.
    • misc: Operators outside the standard finite difference context.
    • performance: Deep dives into Devito optimizations, steering the optimization process, and GPU execution.

    Developer Internals

    • compiler: Notebooks exploring the Devito compiler architecture (experimental/early stage).
  3. What Devito is and is not

    main

    Devito is a Python-based domain-specific language (DSL) and code generation framework for solving systems of partial differential equations (PDEs) through symbolic computation. It allows you to define equations in pure Python using SymPy-powered syntax and automatically generates optimized C/C++ code for various architectures (CPUs with OpenMP/MPI, and accelerators like CUDA, HIP, SYCL, and OpenACC).

    Key Distinctions:

    • It is NOT a seismic modeling framework: While often used in seismic imaging, Devito is physics-agnostic. It is a general-purpose PDE solver engine.
    • It is NOT a set of pre-baked solvers: You provide the physics and the numerical schemes; Devito provides the high-performance implementation.
  4. How Devito computes GFlops/s and GPts/s

    main

    Devito estimates performance using a combination of compile-time symbolic analysis and runtime execution data:

    • Execution Time: Section times are measured via cheap timers in the generated code. Total Operator time is measured via Python-level timers (including op.apply(...) overhead).
    • Floating-Point Operations (FLOPs): Devito uses an in-house estimator (rather than SymPy's) to count operations after symbolic flop-reducing transformations. It ignores the cost of integer arithmetic used for multi-dimensional array offset indexing.
    • GFlops/s Calculation: Devito multiplies the estimated FLOPs by the size of the iteration space at the granularity of individual expressions. For example, in a nested loop, the total operations are calculated by instantiating the loop bounds (e.g., $x_M - x_m + 1$) at runtime.

    While these metrics are intended for a realistic initial indication of performance, they are highly correlated with professional profilers like Intel Advisor.

  5. Ensure MPI safety in Devito code

    main

    When writing code intended for MPI execution, you must distinguish between the global domain shape and the local domain shape (the data shape after decomposition).

    • Always use grid.shape (or f.grid.shape for a function f) to refer to the actual global dimensions of the problem.
    • Never use f.shape or f.data.shape to determine global dimensions, as these return the local shape specific to an MPI rank.
  6. Customize MPI domain decomposition topology

    main

    By default, Devito decomposes the domain starting from the slowest axis (the outermost dimension). You can override this behavior using the topology argument in the Grid class.

    Passing an n-tuple to topology specifies how many partitions are created for each dimension.

    Wildcard Support: You can use the '*' wildcard to tell Devito to decompose a dimension into $N$ chunks, where $N$ is the number of MPI ranks at runtime. If $N$ cannot be evenly divided, Devito prioritizes the outermost dimensions to ensure the decomposition is as even as possible.

    Examples for a 3D grid (x, y, z):

    • (1, 1, 4): Decomposes only the $z$ dimension into 4 chunks.
    • (1, '*', 1): Decomposes the $y$ dimension into $N$ chunks.
    • ('*', '*', 1): Decomposes $x$ and $y$ dimensions into factors of $N$.
    # Example: 2D grid with 4 partitions on the x-axis and replication on y
    Grid(shape=(16, 16), topology=(4, 1))
    
    # Example: 3D grid with wildcard decomposition
    Grid(shape=(16, 16, 16), topology=(1, '*', 1))
  7. Understand the difference between f() and f[] notation

    main

    Devito uses two primary ways to express finite difference operators:

    1. f() notation (Functional): The standard, safe way to express derivatives and stencil expressions. It is designed to ensure only legal stencil expressions are built.
    2. f[] notation (Indexed): Used for accessing specific grid points or escaping the constraints of the functional language. Warning: This notation is not safe and can easily lead to out-of-bounds memory accesses. Use it judiciously.

    You can mix both notations when constructing equations (Eq).

    >>> from devito import Grid, Function
    >>> grid = Grid(shape=(5, 6))
    >>> f = Function(name='f', grid=grid, space_order=2)
    >>> f.dx
    Derivative(f(x, y), x)
    >>> f.dx.evaluate
    -f(x, y)/h_x + f(x + h_x, y)/h_x
    
    >>> x, y = grid.dimensions
    >>> f[x + 1000, y]
    f[x + 1000, y]
    
    >>> f.dx + f[x + 1000, y]
    Derivative(f(x, y), x) + f[x + 1000, y]
  8. GIL behavior in Devito Operators

    main
    Devito Operators release the Python Global Interpreter Lock (GIL) when executing the generated C code. This is achieved by using ctypes.CDLL to call the JIT-compiled code, allowing for better multi-threaded performance in Python environments.
  9. Subclass Devito types using Reconstructable

    main

    When subclassing complex types like Function or SparseTimeFunction, you must handle Devito's 'reconstruction' mechanism. Because Devito objects are immutable, symbolic transformations create new objects.

    To allow Devito to correctly rebuild your custom objects during compilation, your class must inherit from Reconstructable and define __rargs__ (for positional arguments) and __rkwargs__ (for keyword arguments) that match your __init__ method.

    from devito import Reconstructable
    
    class Foo(Reconstructable):
        __rargs__ = ('a', 'b')
        __rkwargs__ = ('c',)
    
        def __init__(self, a, b, c=4):
               self.a = a
               self.b = b
               self.c = c
    
        def __repr__(self):
            return "x(%d, %d)" % (self.a, self.b)
    
    # Usage of reconstruction:
    a = Foo(3, 5)
    a._rebuild(c=5) # Returns a new object: x(3, 5) with c=5
  10. Understanding equation ordering in generated code

    main
    If the equations in your generated C code appear in a different order than they were defined in your Operator, this is expected behavior. The Devito compiler performs a topological ordering based on data dependency analysis and may heuristically reorder equations to improve performance (e.g., to enhance data locality).
  11. How Devito optimizes complex expressions

    main

    Devito applies several symbolic optimizations to reduce the 'operation count' (FLOPs) in complex expressions. Key optimizations include:

    • Factorization: Attempting to factor out common terms to reduce redundant floating-point operations.
    • Common Sub-expression Elimination (CSE): Identifying and reusing identical computations.
    • Loop-invariant hoisting: Moving computations out of loops if they don't change across iterations.
    • Cross-iteration redundancy detection: Optimizing high-order derivatives.

    While Devito aims for optimal factorization, some missed opportunities may exist. However, these typically only result in slightly higher runtimes if the code is not heavily compute-bound, and numerical outputs remain indistinguishable to machine precision.