patrick-kidger / patrick-kidger/diffrax

[question] Computational complexity of integrating / backpropogating through SDE

Open
#186 11 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

question
Dominant language
Python
Stars
2.1k
Forks
189
Avg merge
3d 18h
Merged PRs (30d)
1

Description

Hi, I was a bit surprised by the difference in speed between backpropogating through ODEs vs SDEs but couldn't find any discussion in the documentation about the time complexity of either. With a quick look, I couldn't find any issues addressing this. In particular, consider this piece of code, where we vary the different terms being integrated:

import optax
import jax
import jax.numpy as jnp
import jax.random as jrandom
import equinox as eqx
import diffrax as dx
from tqdm import tqdm

def sample(model, num_steps, gamma, dim, key):
    drift, diff = model

    def f(t, y, args):
        return drift(jnp.concatenate([t[None], y]))

    def g(t, y, args):
        return diff(jnp.concatenate([t[None], y]))**2

    control = dx.VirtualBrownianTree(
        t0=0,
        t1=1,
        tol=1/(2*num_steps),
        shape=(dim,),
        key=key,
    )

    drift_term = dx.ODETerm(f)
    diffusion_term = dx.WeaklyDiagonalControlTerm(g, control)
    terms = dx.MultiTerm(drift_term, diffusion_term)
    solver = dx.Euler()
    y0 = jnp.zeros(dim)

    ts = jnp.linspace(0, 1, num_steps + 1)
    saveat = dx.SaveAt(ts=ts)

    sol = dx.diffeqsolve(
        terms,
        # diffusion_term,
        # drift_term,
        solver,
        0,
        1,
        1/num_steps,
        y0,
        saveat=saveat,
        max_steps=num_steps + 1,
    )

    return sol.ys

def loss(drift, num_steps, gamma, dim, key):
    path = sample(drift, num_steps, gamma, dim, key)
    final = path[-1]
    loss = jnp.sum(final**2)
    return loss

@eqx.filter_value_and_grad
def loss_mean(drift, num_steps, gamma, dim, key, batch_size):
    loss_vmapped = jax.vmap(loss, (None, None, None, None, 0), 0)
    key = jrandom.split(key, batch_size)
    return jnp.mean(loss_vmapped(drift, num_steps, gamma, dim, key))


if __name__=="__main__":

    key = jrandom.PRNGKey(0)

    init_drift_key, init_diff_key, train_key = jrandom.split(key, 3)

    dim = 500


    drift = eqx.nn.MLP(dim + 1, dim, 300, 2, key=init_drift_key)
    diff = eqx.nn.MLP(dim + 1, 1, 300, 2, key=init_diff_key)

    model = (drift, diff)

    optimizer = optax.adamw(1e-4)
    opt_state = optimizer.init(eqx.filter(model, eqx.is_inexact_array))
    
    @eqx.filter_jit
    def make_step(model, num_steps, gamma, dim, key, batch_size, opt_state):
        loss, grads = loss_mean(model, num_steps, gamma, dim, key, batch_size)
        updates, opt_state = optimizer.update(
            grads, opt_state, eqx.filter(model, eqx.is_inexact_array)
        )
        model = eqx.apply_updates(model, updates)
        return loss, model, opt_state


    for step in tqdm(range(100)):
        step_key = jrandom.fold_in(train_key, step)
        loss, model, opt_state = make_step(
            model, 20, 0.1, dim, step_key, 32, opt_state
        )

On my machine locally (macbook m1) integrating terms or diffusion_term takes around 41s and integrating drift_term takes around 5s. What is the reason for this difference? Am I doing something wrong here? Note, that the computation in the diffusion term is simply multiplying BM by a scalar. Is VirtualBrownianTree the slow part here? I suspect implementing the euler solver in plain jax would give a faster solution -- would that be wrong? Maybe it's worth adding some documentation about this.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the example's dx.VirtualBrownianTree, dx.MultiTerm, dx.Euler, and dx.diffeqsolve calls, and reproduce the reported timings for the drift, diffusion, and combined terms. Compare the integration and differentiated paths to identify the source of the SDE overhead. Done means documenting the relevant complexity and whether a plain-JAX Euler implementation changes the result.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
machine-learning, performance
Issue type
Documentation
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.