[Python] Why is the JAX and JAXOPT based code so slow?

Discussão em 'Python' iniciado por Stack, Junho 9, 2025.

  1. Stack

    Stack Membro Participativo

    I am writing a jax and jaxopt based optimization. The same code takes ~3-4 seconds in R. I am not sure why this code takes ~90 seconds in jax. I am new to python, jax and jaxopt, any suggestions to include the code quality or speed will be much appreciated.

    Thank you to the community!

    The input file (.csv) is pasted below first.

    ID,AMT,TIME,DV,WT,MDV,EVID
    1,4.02,0,0.74,79.6,1,1
    1,.,0.25,2.84,.,0,0
    1,.,0.57,6.57,.,0,0
    1,.,1.12,10.5,.,0,0
    1,.,2.02,9.66,.,0,0
    1,.,3.82,8.58,.,0,0
    1,.,5.1,8.36,.,0,0
    1,.,7.03,7.47,.,0,0
    1,.,9.05,6.89,.,0,0
    1,.,12.12,5.94,.,0,0
    1,.,24.37,3.28,.,0,0
    2,4.4,0,0,72.4,1,1
    2,.,0.27,1.72,.,0,0
    2,.,0.52,7.91,.,0,0
    2,.,1,8.31,.,0,0
    2,.,1.92,8.33,.,0,0
    2,.,3.5,6.85,.,0,0
    2,.,5.02,6.08,.,0,0
    2,.,7.03,5.4,.,0,0
    2,.,9,4.55,.,0,0
    2,.,12,3.01,.,0,0
    2,.,24.3,0.9,.,0,0
    3,4.53,0,0,70.5,1,1
    3,.,0.27,4.4,.,0,0
    3,.,0.58,6.9,.,0,0
    3,.,1.02,8.2,.,0,0
    3,.,2.02,7.8,.,0,0
    3,.,3.62,7.5,.,0,0
    3,.,5.08,6.2,.,0,0
    3,.,7.07,5.3,.,0,0
    3,.,9,4.9,.,0,0
    3,.,12.15,3.7,.,0,0
    3,.,24.17,1.05,.,0,0
    4,4.4,0,0,72.7,1,1
    4,.,0.35,1.89,.,0,0
    4,.,0.6,4.6,.,0,0
    4,.,1.07,8.6,.,0,0
    4,.,2.13,8.38,.,0,0
    4,.,3.5,7.54,.,0,0
    4,.,5.02,6.88,.,0,0
    4,.,7.02,5.78,.,0,0
    4,.,9.02,5.33,.,0,0
    4,.,11.98,4.19,.,0,0
    4,.,24.65,1.15,.,0,0
    5,5.86,0,0,54.6,1,1
    5,.,0.3,2.02,.,0,0
    5,.,0.52,5.63,.,0,0
    5,.,1,11.4,.,0,0
    5,.,2.02,9.33,.,0,0
    5,.,3.5,8.74,.,0,0
    5,.,5.02,7.56,.,0,0
    5,.,7.02,7.09,.,0,0
    5,.,9.1,5.9,.,0,0
    5,.,12,4.37,.,0,0
    5,.,24.35,1.57,.,0,0
    6,4.,0,0,80.,1,1
    6,.,0.27,1.29,.,0,0
    6,.,0.58,3.08,.,0,0
    6,.,1.15,6.44,.,0,0
    6,.,2.03,6.32,.,0,0
    6,.,3.57,5.53,.,0,0
    6,.,5,4.94,.,0,0
    6,.,7,4.02,.,0,0
    6,.,9.22,3.46,.,0,0
    6,.,12.1,2.78,.,0,0
    6,.,23.85,0.92,.,0,0
    7,4.95,0,0.15,64.6,1,1
    7,.,0.25,0.85,.,0,0
    7,.,0.5,2.35,.,0,0
    7,.,1.02,5.02,.,0,0
    7,.,2.02,6.58,.,0,0
    7,.,3.48,7.09,.,0,0
    7,.,5,6.66,.,0,0
    7,.,6.98,5.25,.,0,0
    7,.,9,4.39,.,0,0
    7,.,12.05,3.53,.,0,0
    7,.,24.22,1.15,.,0,0
    8,4.53,0,0,70.5,1,1
    8,.,0.25,3.05,.,0,0
    8,.,0.52,3.05,.,0,0
    8,.,0.98,7.31,.,0,0
    8,.,2.02,7.56,.,0,0
    8,.,3.53,6.59,.,0,0
    8,.,5.05,5.88,.,0,0
    8,.,7.15,4.73,.,0,0
    8,.,9.07,4.57,.,0,0
    8,.,12.1,3,.,0,0
    8,.,24.12,1.25,.,0,0
    9,3.1,0,0,86.4,1,1
    9,.,0.3,7.37,.,0,0
    9,.,0.63,9.03,.,0,0
    9,.,1.05,7.14,.,0,0
    9,.,2.02,6.33,.,0,0
    9,.,3.53,5.66,.,0,0
    9,.,5.02,5.67,.,0,0
    9,.,7.17,4.24,.,0,0
    9,.,8.8,4.11,.,0,0
    9,.,11.6,3.16,.,0,0
    9,.,24.43,1.12,.,0,0
    10,5.5,0,0.24,58.2,1,1
    10,.,0.37,2.89,.,0,0
    10,.,0.77,5.22,.,0,0
    10,.,1.02,6.41,.,0,0
    10,.,2.05,7.83,.,0,0
    10,.,3.55,10.21,.,0,0
    10,.,5.05,9.18,.,0,0
    10,.,7.08,8.02,.,0,0
    10,.,9.38,7.14,.,0,0
    10,.,12.1,5.68,.,0,0
    10,.,23.7,2.42,.,0,0
    11,4.92,0,0,65.,1,1
    11,.,0.25,4.86,.,0,0
    11,.,0.5,7.24,.,0,0
    11,.,0.98,8,.,0,0
    11,.,1.98,6.81,.,0,0
    11,.,3.6,5.87,.,0,0
    11,.,5.02,5.22,.,0,0
    11,.,7.03,4.45,.,0,0
    11,.,9.03,3.62,.,0,0
    11,.,12.12,2.69,.,0,0
    11,.,24.08,0.86,.,0,0
    12,5.3,0,0,60.5,1,1
    12,.,0.25,1.25,.,0,0
    12,.,0.5,3.96,.,0,0
    12,.,1,7.82,.,0,0
    12,.,2,9.72,.,0,0
    12,.,3.52,9.75,.,0,0
    12,.,5.07,8.57,.,0,0
    12,.,7.07,6.59,.,0,0
    12,.,9.03,6.11,.,0,0
    12,.,12.05,4.57,.,0,0
    12,.,24.15,1.17,.,0,0


    Code for optimization

    ## using LBFGSB from jaxopt for optimization

    import jax
    import jax.numpy as jnp
    import pandas as pd
    import jaxopt as jaxopt
    import time
    # import os
    # os.environ["JAX_ENABLE_X64"] = "True"
    jax.config.update("jax_enable_x64", True) # Enable 64-bit precision


    # Calculate the sqrt of the inverse of a matrix
    @jax.jit
    def mat_sqrt_inv(mat):
    eigvals, eigvecs = jnp.linalg.eigh(mat)
    d2 = 1.0 / jnp.sqrt(jnp.abs(eigvals))
    return eigvecs @ jnp.diag(d2) @ eigvecs.T

    # Calculate the PRED value
    @jax.jit
    def PRED(THETA, ETA, DATA):
    DOSE = 320.0
    TIME = DATA[:, 1] # TIME values

    KA = THETA[0] * jnp.exp(ETA[0])
    V = THETA[1] * jnp.exp(ETA[1])
    K = THETA[2] * jnp.exp(ETA[2])

    F = DOSE / V * KA / (KA - K) * (jnp.exp(-K * TIME) - jnp.exp(-KA * TIME))

    G1 = ((DOSE / V / (KA - K) - DOSE / V * KA / (KA - K) ** 2) * (jnp.exp(-K * TIME) - jnp.exp(-KA * TIME)) + DOSE / V * KA / (KA - K) * (jnp.exp(-KA * TIME) * TIME)) * KA
    G2 = -(DOSE / V ** 2 * KA / (KA - K) * (jnp.exp(-K * TIME) - jnp.exp(-KA * TIME))) * V
    G3 = (DOSE / V * KA / (KA - K) ** 2 * (jnp.exp(-K * TIME) - jnp.exp(-KA * TIME)) - DOSE / V * KA / (KA - K) * (jnp.exp(-K * TIME) * TIME)) * K
    H1 = F
    H2 = jnp.ones_like(F)

    return jnp.stack([F, G1, G2, G3, H1, H2], axis=1)

    # Read data
    # Read data
    df = pd.read_csv("THEOPH.csv")
    df = df[df['EVID'] == 0]
    ID = df['ID'].unique()
    NETA = 3
    NEPS = 2

    selected_columns = ['ID', 'TIME', 'DV']
    DATASET = jnp.array(df[selected_columns].values)

    _subject_data_list = []
    for subject_id_val in sorted(ID): # Iterate through unique IDs in sorted order
    _subject_data_list.append(DATASET[DATASET[:, 0] == subject_id_val])

    # Final NONMEM estimates
    THETA = jnp.array([2.8864797758451806, 33.693922520723312, 8.7260954693777815E-002])
    OMEGA = jnp.array([
    [0.88639937186299267, 0, 0],
    [0, 2.0903952408947064E-002, 0],
    [0, 0, 6.8919408612682198E-002]
    ])
    SIGMA = jnp.array([
    [9.9259732296116312E-003, 0],
    [0, 3.2931599977662498E-002]
    ])


    # @jax.jit
    def obj_fn(params, DATASET):
    THETA = params[:3]
    OMEGA_diag = params[3:6]
    SIGMA_diag = params[6:8]


    OBJ = 0.0
    for i in ID:
    # Select the data for the current subject
    DATA = _subject_data_list[i-1]

    # DATA = select_id(DATASET, i)
    ETA = jnp.zeros(NETA)
    FGH = PRED(THETA, ETA, DATA)
    Y = DATA[:, 2] # DV values
    Fpred = FGH[:, 0]
    G = FGH[:, 1:4]
    H = FGH[:, 4:6]

    OM = jnp.diag(OMEGA_diag)
    SG = jnp.diag(SIGMA_diag)
    RES = Y - Fpred
    COV = G @ OM @ G.T + jnp.diag(jnp.diag(H @ SG @ H.T))
    # Add small value to diagonal for numerical stability
    COV += jnp.eye(COV.shape[0]) * 1e-8
    # WRES = mat_sqrt_inv(COV) @ RES
    # OBJI = jnp.log(jnp.linalg.det(COV)) + WRES.T @ WRES
    OBJI = jnp.log(jnp.linalg.det(COV)) + RES @ jnp.linalg.solve(COV, RES)
    OBJ += OBJI

    # print(f"Objective function: {OBJ}")
    return jnp.array(OBJ, dtype=jnp.float32)

    # JIT the objective function for performance
    obj_fn_jit = jax.jit(obj_fn)

    # Objective function from final NONMEM estimates
    # print("Objective function from final NONMEM estimates:")
    # print(obj_fn(jnp.concatenate([THETA, OMEGA.diagonal(), SIGMA.diagonal()]), DATASET))
    print(obj_fn_jit(jnp.concatenate([THETA, OMEGA.diagonal(), SIGMA.diagonal()]), DATASET))
    # Initial parameters
    initpar = jnp.array([
    3.0, 35.0, 0.09, # THETA
    0.9, 0.02, 0.06, # OMEGA diagonal
    0.01, 0.03 # SIGMA diagonal
    ])

    # print("Objective function from initial parameters:")
    # print(obj_fn_jit(initpar, DATASET))

    def obj_fn_grad(params, DATASET):
    # Compute both value and gradient of the objective function
    value, grad = jax.value_and_grad(obj_fn, argnums=0)(params, DATASET)
    return(value, grad)

    obj_fn_grad_jit = jax.jit(obj_fn_grad)
    # print("Objective function and gradient from initial parameters:")
    # print(obj_fn_grad_jit(initpar, DATASET))


    # 2. Optimizer Initialization
    optimizer = jaxopt.LBFGSB(
    # fun=obj_fn_jit, # Objective function to minimize
    fun=obj_fn_grad_jit, # Objective function to minimize
    value_and_grad=True, # Return both value and gradient
    tol=1e-6, # Tolerance for convergence
    # maxiter=1000, # Maximum number of iterations
    # verbose=True, # Print optimization progress
    jit=True
    )

    l_bound = jnp.array([
    0.0, # Lower bound for THETA[0]
    0.0, # Lower bound for THETA[1]
    0.0, # Lower bound for THETA[2]
    -10.0, # Lower bound for OMEGA[0,0]
    -10.0, # Lower bound for OMEGA[1,1]
    -10.0, # Lower bound for OMEGA[2,2]
    -10.0, # Lower bound for SIGMA[0,0]
    -10.0 # Lower bound for SIGMA[1,1]
    ])
    u_bound = jnp.array([
    100.0, # Upper bound for THETA[0]
    100.0, # Upper bound for THETA[1]
    100.0, # Upper bound for THETA[2]
    10.0, # Upper bound for OMEGA[0,0]
    10.0, # Upper bound for OMEGA[1,1]
    10.0, # Upper bound for OMEGA[2,2]
    10.0, # Upper bound for SIGMA[0,0]
    10.0 # Upper bound for SIGMA[1,1]
    ])

    # 3. Step function for optimization
    # opt_state = optimizer.init(initpar)
    start_time = time.time()
    solution = optimizer.run(init_params=initpar, bounds=(l_bound, u_bound), DATASET=DATASET)
    execution_time = time.time() - start_time
    print(f"Optimization completed in {execution_time:.4f} seconds.")
    print("Final parameter values:",solution.params)
    print("Final objective function value:", solution.state.value)


    ## To print the optimization progress step by step ---------------------------
    current_params = initpar

    # Initialize the optimizer's state.
    # DATASET and bounds are passed as they are needed by the objective function
    # or the LBFGSB algorithm during initialization and updates.
    # The fun (obj_fn_grad_jit) will be called here to get the initial value and grad.
    opt_state = optimizer.init_state(current_params, bounds=(l_bound, u_bound), DATASET=DATASET)

    max_total_updates = 1000 # Define a maximum number of total updates for our loop
    print_every_n_updates = 5
    total_updates_done = 0

    print(f"Initial parameters: {current_params}")
    print(f"Initial objective value: {opt_state.value:.6f}")
    if hasattr(opt_state, 'grad') and opt_state.grad is not None:
    print(f"Initial gradient norm: {jnp.linalg.norm(opt_state.grad):.6e}")
    print(f"Initial optimality error: {opt_state.error:.6e}")

    start_time = time.time()

    # Outer loop: controls how many times we print results
    for reporting_step in range(max_total_updates // print_every_n_updates + 1):
    if total_updates_done >= max_total_updates:
    break

    # Inner loop: perform `print_every_n_updates`
    for _ in range(print_every_n_updates):
    if total_updates_done >= max_total_updates:
    break

    current_params, opt_state = optimizer.update(
    params=current_params,
    state=opt_state,
    bounds=(l_bound, u_bound),
    DATASET=DATASET
    )
    total_updates_done += 1

    if opt_state.error < optimizer.tol: # Check for convergence after each step
    break

    print(f"\n--- After {total_updates_done} updates (Reporting step {reporting_step + 1}) ---")
    print(f" Current parameters: {current_params}")
    print(f" Objective value: {opt_state.value:.6f}")
    print(f" Optimality error: {opt_state.error:.6e}")
    print(f" Solver iterations (total LBFGSB updates): {opt_state.iter_num}")

    if opt_state.error < optimizer.tol:
    print(f"\nConvergence reached after {total_updates_done} updates.")
    break

    execution_time = time.time() - start_time
    print(f"Optimization completed in {execution_time:.4f} seconds.")
    print(f"Final parameter values: {solution.params}")
    print(f"Final objective function value: {solution.state.value:.6f}")

    Continue reading...

Compartilhe esta Página