Skip to content

Commit 9eec46a

Browse files
Merge pull request #978 from codeflash-ai/optimization-effort
Optimization effort
2 parents 2db9549 + 487d7a0 commit 9eec46a

7 files changed

Lines changed: 112 additions & 85 deletions

File tree

codeflash/api/aiservice.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
from codeflash.code_utils.env_utils import get_codeflash_api_key
1717
from codeflash.code_utils.git_utils import get_last_commit_author_if_pr_exists, get_repo_owner_and_name
1818
from codeflash.code_utils.time_utils import humanize_runtime
19-
from codeflash.lsp.helpers import is_LSP_enabled
2019
from codeflash.models.ExperimentMetadata import ExperimentMetadata
2120
from codeflash.models.models import (
2221
AIServiceRefinerRequest,
@@ -131,6 +130,7 @@ def optimize_python_code( # noqa: D417
131130
experiment_metadata: ExperimentMetadata | None = None,
132131
*,
133132
is_async: bool = False,
133+
n_candidates: int = 5,
134134
) -> list[OptimizedCandidate]:
135135
"""Optimize the given python code for performance by making a request to the Django endpoint.
136136
@@ -141,6 +141,7 @@ def optimize_python_code( # noqa: D417
141141
- trace_id (str): Trace id of optimization run
142142
- experiment_metadata (Optional[ExperimentalMetadata, None]): Any available experiment metadata for this optimization
143143
- is_async (bool): Whether the function being optimized is async
144+
- n_candidates (int): Number of candidates to generate
144145
145146
Returns
146147
-------
@@ -163,10 +164,10 @@ def optimize_python_code( # noqa: D417
163164
"repo_owner": git_repo_owner,
164165
"repo_name": git_repo_name,
165166
"is_async": is_async,
166-
"lsp_mode": is_LSP_enabled(),
167167
"call_sequence": self.get_next_sequence(),
168+
"n_candidates": n_candidates,
168169
}
169-
logger.debug(f"Sending optimize request: trace_id={trace_id}, lsp_mode={payload['lsp_mode']}")
170+
logger.debug(f"Sending optimize request: trace_id={trace_id}, n_candidates={payload['n_candidates']}")
170171

171172
try:
172173
response = self.make_ai_service_request("/optimize", payload=payload, timeout=self.timeout)
@@ -198,6 +199,7 @@ def optimize_python_code_line_profiler( # noqa: D417
198199
dependency_code: str,
199200
trace_id: str,
200201
line_profiler_results: str,
202+
n_candidates: int,
201203
experiment_metadata: ExperimentMetadata | None = None,
202204
) -> list[OptimizedCandidate]:
203205
"""Optimize the given python code for performance using line profiler results.
@@ -209,6 +211,7 @@ def optimize_python_code_line_profiler( # noqa: D417
209211
- trace_id (str): Trace id of optimization run
210212
- line_profiler_results (str): Line profiler output to guide optimization
211213
- experiment_metadata (Optional[ExperimentalMetadata, None]): Any available experiment metadata for this optimization
214+
- n_candidates (int): Number of candidates to generate
212215
213216
Returns
214217
-------
@@ -225,12 +228,12 @@ def optimize_python_code_line_profiler( # noqa: D417
225228
payload = {
226229
"source_code": source_code,
227230
"dependency_code": dependency_code,
231+
"n_candidates": n_candidates,
228232
"line_profiler_results": line_profiler_results,
229233
"trace_id": trace_id,
230234
"python_version": platform.python_version(),
231235
"experiment_metadata": experiment_metadata,
232236
"codeflash_version": codeflash_version,
233-
"lsp_mode": is_LSP_enabled(),
234237
"call_sequence": self.get_next_sequence(),
235238
}
236239

codeflash/cli_cmds/cli.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ def parse_args() -> Namespace:
107107
action="store_true",
108108
help="(Deprecated) Async function optimization is now enabled by default. This flag is ignored.",
109109
)
110+
parser.add_argument(
111+
"--effort", type=str, help="Effort level for optimization", choices=["low", "medium", "high"], default="medium"
112+
)
110113

111114
args, unknown_args = parser.parse_known_args()
112115
sys.argv[:] = [sys.argv[0], *unknown_args]
Lines changed: 66 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1+
from __future__ import annotations
2+
3+
from enum import Enum
4+
from typing import Any, Union
5+
16
MAX_TEST_RUN_ITERATIONS = 5
27
INDIVIDUAL_TESTCASE_TIMEOUT = 15
38
MAX_FUNCTION_TEST_SECONDS = 60
4-
N_CANDIDATES = 5
59
MIN_IMPROVEMENT_THRESHOLD = 0.05
610
MIN_THROUGHPUT_IMPROVEMENT_THRESHOLD = 0.10 # 10% minimum improvement for async throughput
711
MAX_TEST_FUNCTION_RUNS = 50
812
MAX_CUMULATIVE_TEST_RUNTIME_NANOSECONDS = 100e6 # 100ms
9-
N_TESTS_TO_GENERATE = 2
1013
TOTAL_LOOPING_TIME = 10.0 # 10 second candidate benchmarking budget
1114
COVERAGE_THRESHOLD = 60.0
1215
MIN_TESTCASE_PASSED_THRESHOLD = 6
1316
REPEAT_OPTIMIZATION_PROBABILITY = 0.1
1417
DEFAULT_IMPORTANCE_THRESHOLD = 0.001
15-
N_CANDIDATES_LP = 6
1618

1719
# pytest loop stability
1820
# For now, we use strict thresholds (large windows and low tolerances), since this is still experimental.
@@ -21,44 +23,82 @@
2123
STABILITY_SPREAD_TOLERANCE = 0.0025 # 0.25% window spread
2224

2325
# Refinement
24-
REFINE_ALL_THRESHOLD = 2 # when valid optimizations count is 2 or less, refine all optimizations
2526
REFINED_CANDIDATE_RANKING_WEIGHTS = (2, 1) # (runtime, diff), runtime is more important than diff by a factor of 2
26-
TOP_N_REFINEMENTS = 0.45 # top 45% of valid optimizations (based on the weighted score) are refined
2727

2828
# LSP-specific
29-
N_CANDIDATES_LSP = 3
30-
N_TESTS_TO_GENERATE_LSP = 2
3129
TOTAL_LOOPING_TIME_LSP = 10.0 # Kept same timing for LSP mode to avoid in increase in performance reporting
32-
N_CANDIDATES_LP_LSP = 3
3330

3431
# setting this value to 1 will disable repair if there is at least one correct candidate
3532
MIN_CORRECT_CANDIDATES = 2
3633

37-
# Code repair
38-
REPAIR_UNMATCHED_PERCENTAGE_LIMIT = 0.4 # if the percentage of unmatched tests is greater than this, we won't fix it (lowering this value makes the repair more stricted)
39-
MAX_REPAIRS_PER_TRACE = 4 # maximum number of repairs we will do for each function
40-
41-
# Adaptive optimization
42-
# TODO (ali): make this configurable with effort arg once the PR is merged
43-
ADAPTIVE_OPTIMIZATION_THRESHOLD = 2 # Max adaptive optimizations per single candidate tree (for example : optimize -> refine -> adaptive -> another adaptive).
44-
# MAX_ADAPTIVE_OPTIMIZATIONS_PER_TRACE = 4 # maximum number of adaptive optimizations we will do for each function (this can be 2 adaptive optimizations for 2 candidates for example)
45-
MAX_ADAPTIVE_OPTIMIZATIONS_PER_TRACE = (
46-
0 # disable adaptive optimizations until we have this value controlled by the effort arg
47-
)
48-
49-
MAX_N_CANDIDATES = 5
50-
MAX_N_CANDIDATES_LP = 6
51-
5234
try:
5335
from codeflash.lsp.helpers import is_LSP_enabled
5436

5537
_IS_LSP_ENABLED = is_LSP_enabled()
5638
except ImportError:
5739
_IS_LSP_ENABLED = False
5840

59-
N_CANDIDATES_EFFECTIVE = min(N_CANDIDATES_LSP if _IS_LSP_ENABLED else N_CANDIDATES, MAX_N_CANDIDATES)
60-
N_CANDIDATES_LP_EFFECTIVE = min(N_CANDIDATES_LP_LSP if _IS_LSP_ENABLED else N_CANDIDATES_LP, MAX_N_CANDIDATES_LP)
61-
N_TESTS_TO_GENERATE_EFFECTIVE = N_TESTS_TO_GENERATE_LSP if _IS_LSP_ENABLED else N_TESTS_TO_GENERATE
6241
TOTAL_LOOPING_TIME_EFFECTIVE = TOTAL_LOOPING_TIME_LSP if _IS_LSP_ENABLED else TOTAL_LOOPING_TIME
6342

6443
MAX_CONTEXT_LEN_REVIEW = 1000
44+
45+
46+
class EffortLevel(str, Enum):
47+
LOW = "low"
48+
MEDIUM = "medium"
49+
HIGH = "high"
50+
51+
52+
class EffortKeys(str, Enum):
53+
N_OPTIMIZER_CANDIDATES = "N_OPTIMIZER_CANDIDATES"
54+
N_OPTIMIZER_LP_CANDIDATES = "N_OPTIMIZER_LP_CANDIDATES"
55+
N_GENERATED_TESTS = "N_GENERATED_TESTS"
56+
MAX_CODE_REPAIRS_PER_TRACE = "MAX_CODE_REPAIRS_PER_TRACE"
57+
REPAIR_UNMATCHED_PERCENTAGE_LIMIT = "REPAIR_UNMATCHED_PERCENTAGE_LIMIT"
58+
TOP_VALID_CANDIDATES_FOR_REFINEMENT = "TOP_VALID_CANDIDATES_FOR_REFINEMENT"
59+
ADAPTIVE_OPTIMIZATION_THRESHOLD = "ADAPTIVE_OPTIMIZATION_THRESHOLD"
60+
MAX_ADAPTIVE_OPTIMIZATIONS_PER_TRACE = "MAX_ADAPTIVE_OPTIMIZATIONS_PER_TRACE"
61+
62+
63+
EFFORT_VALUES: dict[str, dict[EffortLevel, Any]] = {
64+
EffortKeys.N_OPTIMIZER_CANDIDATES.value: {EffortLevel.LOW: 3, EffortLevel.MEDIUM: 5, EffortLevel.HIGH: 6},
65+
EffortKeys.N_OPTIMIZER_LP_CANDIDATES.value: {EffortLevel.LOW: 4, EffortLevel.MEDIUM: 6, EffortLevel.HIGH: 7},
66+
# we don't use effort with generated tests for now
67+
EffortKeys.N_GENERATED_TESTS.value: {EffortLevel.LOW: 2, EffortLevel.MEDIUM: 2, EffortLevel.HIGH: 2},
68+
# maximum number of repairs we will do for each function (in case the valid candidates is less than MIN_CORRECT_CANDIDATES)
69+
EffortKeys.MAX_CODE_REPAIRS_PER_TRACE.value: {EffortLevel.LOW: 2, EffortLevel.MEDIUM: 3, EffortLevel.HIGH: 5},
70+
# if the percentage of unmatched tests is greater than this, we won't fix it (lowering this value makes the repair more stricted)
71+
# on the low effort we lower the limit to 20% to be more strict (less repairs, less time)
72+
EffortKeys.REPAIR_UNMATCHED_PERCENTAGE_LIMIT.value: {
73+
EffortLevel.LOW: 0.2,
74+
EffortLevel.MEDIUM: 0.3,
75+
EffortLevel.HIGH: 0.4,
76+
},
77+
# Top valid candidates for refinements
78+
EffortKeys.TOP_VALID_CANDIDATES_FOR_REFINEMENT: {EffortLevel.LOW: 2, EffortLevel.MEDIUM: 3, EffortLevel.HIGH: 4},
79+
# max number of adaptive optimization calls to make per a single candidates tree
80+
EffortKeys.ADAPTIVE_OPTIMIZATION_THRESHOLD.value: {EffortLevel.LOW: 0, EffortLevel.MEDIUM: 0, EffortLevel.HIGH: 2},
81+
# max number of adaptive optimization calls to make per a single trace
82+
EffortKeys.MAX_ADAPTIVE_OPTIMIZATIONS_PER_TRACE.value: {
83+
EffortLevel.LOW: 0,
84+
EffortLevel.MEDIUM: 0,
85+
EffortLevel.HIGH: 4,
86+
},
87+
}
88+
89+
90+
def get_effort_value(key: EffortKeys, effort: Union[EffortLevel, str]) -> Any: # noqa: ANN401
91+
key_str = key.value
92+
93+
if isinstance(effort, str):
94+
try:
95+
effort = EffortLevel(effort)
96+
except ValueError:
97+
msg = f"Invalid effort level: {effort}"
98+
raise ValueError(msg) from None
99+
100+
if key_str not in EFFORT_VALUES:
101+
msg = f"Invalid key: {key_str}"
102+
raise ValueError(msg)
103+
104+
return EFFORT_VALUES[key_str][effort]

codeflash/code_utils/git_utils.py

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
from __future__ import annotations
22

33
import os
4-
import shutil
5-
import subprocess
64
import sys
7-
import tempfile
85
import time
96
from functools import cache
107
from io import StringIO
@@ -16,7 +13,6 @@
1613
from unidiff import PatchSet
1714

1815
from codeflash.cli_cmds.console import logger
19-
from codeflash.code_utils.config_consts import N_CANDIDATES_EFFECTIVE
2016

2117
if TYPE_CHECKING:
2218
from git import Repo
@@ -195,36 +191,6 @@ def check_and_push_branch(repo: git.Repo, git_remote: str | None = "origin", *,
195191
return True
196192

197193

198-
def create_worktree_root_dir(module_root: Path) -> tuple[Path | None, Path | None]:
199-
git_root = git_root_dir() if check_running_in_git_repo(module_root) else None
200-
worktree_root_dir = Path(tempfile.mkdtemp()) if git_root else None
201-
return git_root, worktree_root_dir
202-
203-
204-
def create_git_worktrees(
205-
git_root: Path | None, worktree_root_dir: Path | None, module_root: Path
206-
) -> tuple[Path | None, list[Path]]:
207-
if git_root and worktree_root_dir:
208-
worktree_root = Path(tempfile.mkdtemp(dir=worktree_root_dir))
209-
worktrees = [Path(tempfile.mkdtemp(dir=worktree_root)) for _ in range(N_CANDIDATES_EFFECTIVE + 1)]
210-
for worktree in worktrees:
211-
subprocess.run(["git", "worktree", "add", "-d", worktree], cwd=module_root, check=True)
212-
else:
213-
worktree_root = None
214-
worktrees = []
215-
return worktree_root, worktrees
216-
217-
218-
def remove_git_worktrees(worktree_root: Path | None, worktrees: list[Path]) -> None:
219-
try:
220-
for worktree in worktrees:
221-
subprocess.run(["git", "worktree", "remove", "-f", worktree], check=True)
222-
except subprocess.CalledProcessError as e:
223-
logger.warning(f"Error removing worktrees: {e}")
224-
if worktree_root:
225-
shutil.rmtree(worktree_root)
226-
227-
228194
def get_last_commit_author_if_pr_exists(repo: Repo | None = None) -> str | None:
229195
"""Return the author's name of the last commit in the current branch if PR_NUMBER is set.
230196

codeflash/lsp/server.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from pygls.lsp.server import LanguageServer
88
from pygls.protocol import LanguageServerProtocol
99

10+
from codeflash.code_utils.config_consts import EffortLevel
1011
from codeflash.either import Result
1112
from codeflash.models.models import CodeOptimizationContext
1213

@@ -37,6 +38,7 @@ def prepare_optimizer_arguments(self, config_file: Path) -> None:
3738
args.config_file = config_file
3839
args.no_pr = True # LSP server should not create PRs
3940
args.worktree = True
41+
args.effort = EffortLevel.LOW.value # low effort for high speed
4042
self.args = args
4143
# avoid initializing the optimizer during initialization, because it can cause an error if the api key is invalid
4244

0 commit comments

Comments
 (0)