Skip to content

Add _prev_waiting_trial_number in InMemoryStorage to improve the efficiency of _pop_waiting_trial_id#5993

Merged
HideakiImamura merged 5 commits intooptuna:masterfrom
sawa3030:fix/speed-up-pop-waiting-trial-id-1
Mar 6, 2025
Merged

Add _prev_waiting_trial_number in InMemoryStorage to improve the efficiency of _pop_waiting_trial_id#5993
HideakiImamura merged 5 commits intooptuna:masterfrom
sawa3030:fix/speed-up-pop-waiting-trial-id-1

Conversation

@sawa3030
Copy link
Copy Markdown
Collaborator

@sawa3030 sawa3030 commented Feb 25, 2025

Motivation

Description of the changes

  • Introduced _prev_waiting_trial_id in InMemoryStorage to improve efficiency of get_all_trials(states = (TrialState.WAITING)).

@sawa3030
Copy link
Copy Markdown
Collaborator Author

sawa3030 commented Feb 26, 2025

Performance Improvement

This change improves performance by reducing the overhead of fetching all trials. Below are benchmark results comparing the current master branch.

Before (master)
image

After (PR)
image

Here is the code used to generate the results above

import optuna
import cProfile

study = optuna.create_study()

def objective(trial):
    x = trial.suggest_uniform("x", -10, 10)
    y = trial.suggest_uniform("y", -10, 10)
    return x**2 + y**2

study.optimize(objective, n_trials=100)

for i in range(-5, 5):
    study.enqueue_trial({"x": i, "y": i})

def profile_objective():
    study.optimize(objective, n_trials=100)

cProfile.run('profile_objective()', 'journal_storage_master_100.prof')
# cProfile.run('profile_objective()', 'journal_storage_fix_100.prof')

@sawa3030 sawa3030 marked this pull request as ready for review February 26, 2025 02:44
@HideakiImamura
Copy link
Copy Markdown
Member

@not522 Could you review this PR?

@sawa3030 sawa3030 changed the title add _prev_waiting_trial_id Add _prev_waiting_trial_number in InMemoryStorage to improve the efficiency of pop_waiting_trials Feb 27, 2025
@sawa3030
Copy link
Copy Markdown
Collaborator Author

Let me show you the performance improvement with various examples.

@sawa3030
Copy link
Copy Markdown
Collaborator Author

sawa3030 commented Feb 27, 2025

Performance Improvement

Below are benchmark results comparing the performance before and after this PR using different scenarios.

1. study.optimize() for n_trials = 100, 1000

percall (cumtime / ncalls) of pop_waiting_trials Before (master) After (PR)
n_trials = 100 0.000066482 ± 0.00002 0.000034141 ± 0.000007
n_trials = 1000 0.000241295 ± 0.00001 0.0000334922 ± 0.000004
n_trials = 10000 0.0024929083 0.0000391722

This PR significantly improves performance because _get_all_trials(state=TrialState.WAITING) now only checks the last trial's state instead of iterating over all trials.

Here is the code used to generate this data.

import optuna
import cProfile

study = optuna.create_study()

def objective(trial):
    x = trial.suggest_uniform("x", -10, 10)
    return x**2

def profile_objective():
    study.optimize(objective, n_trials=num_of_trials)

profiller = cProfile.Profile()
profiller.run('profile_objective()')

2. study.enqueue_trial() followed by study.optimize()

percall (cumtime / ncalls) of pop_waiting_trials Before (master) After (PR)
# of enqueue trials = 10 0.0002664 ± 0.0004 0.0001398 ± 0.00004
# of enqueue trials = 100 0.000206 ± 0.00007 0.0003954 ± 0.0002
# of enqueue trials = 1000 0.0007885 ± 0.0008 0.0010906 ± 0.0001

I was not really sure why this test generates some difference.

Here is the code used to generate this data.

import optuna
import cProfile

study = optuna.create_study()

def objective(trial):
    x = trial.suggest_uniform("x", -10, 10)
    return x**2

for i in range(-num_of_waiting_trials // 2, num_of_waiting_trials // 2):
    study.enqueue_trial({"x": i})

def profile_objective():
    study.optimize(objective, n_trials=1)

profiller = cProfile.Profile()
profiller.run('profile_objective()')

3. study.optimize() for n_trials = 500, followed by study.enqueue_trial() for 100 trials, then study.optimize(n_trials=1), profiled with cProfile

percall (cumtime / ncalls) of pop_waiting_trials Before (master) After (PR)
0.0006159 ± 0.0003 0.0004844 ± 0.0003

The performance improvement comes from optimizing the logic: previously, the function checked all 500 + 100 trials, whereas the new implementation only checks 100 trials.

Here is the code used to generate this data.

import optuna
import cProfile

study = optuna.create_study()

def objective(trial):
    x = trial.suggest_uniform("x", -10, 10)
    return x**2

study.optimize(objective, n_trials=500)

for i in range(-50, 50):
    study.enqueue_trial({"x": i})

def profile_objective():
    study.optimize(objective, n_trials=1)

profiller = cProfile.Profile()
profiller.run('profile_objective()')

Benchmarking Details

  • Each test, except for the one with n_trials = 10,000, was conducted 10 times, alternating between the master branch and the PR branch in every iteration (i.e., master → PR → master → PR → ...).
  • The average and standard deviation were computed.
  • Raw data is available here.

@not522
Copy link
Copy Markdown
Member

not522 commented Feb 27, 2025

Could you share benchmarks with larger n_trials (e.g., 10,000 or 50,000)? When it is small, _pop_waiting_trial_id is fast enough to be ignored.

@sawa3030
Copy link
Copy Markdown
Collaborator Author

Sure. But let me do it with n_trials = 10,000 and for a single iteration since my PC isn't powerful enough and doing optimization with larger number of n_trials needs significant amount of time

@sawa3030
Copy link
Copy Markdown
Collaborator Author

@not522 I added the data to the comment above. The improvement is significant—total execution time for pop_waiting_trials (10,000 runs) dropped from 24.929083 seconds to just 0.391722 seconds.

Copy link
Copy Markdown
Member

@not522 not522 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your update! LGTM!

@not522 not522 removed their assignment Feb 28, 2025
@not522 not522 changed the title Add _prev_waiting_trial_number in InMemoryStorage to improve the efficiency of pop_waiting_trials Add _prev_waiting_trial_number in InMemoryStorage to improve the efficiency of _pop_waiting_trial_id Feb 28, 2025
@not522 not522 added code-fix Change that does not change the behavior, such as code refactoring. enhancement Change that does not break compatibility and not affect public interfaces, but improves performance. and removed code-fix Change that does not change the behavior, such as code refactoring. labels Feb 28, 2025
Copy link
Copy Markdown
Member

@HideakiImamura HideakiImamura left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@HideakiImamura HideakiImamura merged commit 0889545 into optuna:master Mar 6, 2025
14 checks passed
@not522 not522 added this to the v4.3.0 milestone Mar 7, 2025
@sawa3030 sawa3030 deleted the fix/speed-up-pop-waiting-trial-id-1 branch November 14, 2025 07:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Change that does not break compatibility and not affect public interfaces, but improves performance.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

_pop_waiting_trial_id for InMemoryStorage is slow

3 participants