llmcomp 1.2.1__tar.gz → 1.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {llmcomp-1.2.1 → llmcomp-1.2.3}/PKG-INFO +1 -1
- {llmcomp-1.2.1 → llmcomp-1.2.3}/llmcomp/question/question.py +26 -1
- {llmcomp-1.2.1 → llmcomp-1.2.3}/llmcomp/runner/runner.py +14 -2
- {llmcomp-1.2.1 → llmcomp-1.2.3}/pyproject.toml +1 -1
- llmcomp-1.2.1/TODO +0 -2
- llmcomp-1.2.1/manager.py +0 -500
- llmcomp-1.2.1/t1.py +0 -66
- llmcomp-1.2.1/ttt.jsonl +0 -10
- {llmcomp-1.2.1 → llmcomp-1.2.3}/.gitignore +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/LICENSE +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/README.md +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/docs/api.md +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/docs/finetuning.md +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/docs/generate_api_docs.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/examples/configuration.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/examples/create_finetuning_job.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/examples/free_form_question.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/examples/ft_old_audubon_birds.jsonl +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/examples/judges.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/examples/model_adapter.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/examples/next_token_question.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/examples/openrouter.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/examples/questions.yaml +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/examples/questions_in_yaml.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/examples/rating_question.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/examples/runner.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/examples/tinker.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/examples/x_mod_57.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/lint.sh +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/llmcomp/__init__.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/llmcomp/config.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/llmcomp/default_adapters.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/llmcomp/finetuning/__init__.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/llmcomp/finetuning/manager.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/llmcomp/finetuning/update_jobs.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/llmcomp/question/judge.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/llmcomp/question/plots.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/llmcomp/question/result.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/llmcomp/runner/chat_completion.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/llmcomp/runner/model_adapter.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/llmcomp/utils.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/scripts/migrate_to_org_id.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/tests/__init__.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/tests/conftest.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/tests/test_config.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/tests/test_hash_and_cache.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/tests/test_question.py +0 -0
- {llmcomp-1.2.1 → llmcomp-1.2.3}/tests/test_utils.py +0 -0
|
@@ -8,7 +8,7 @@ from collections import defaultdict
|
|
|
8
8
|
from concurrent.futures import ThreadPoolExecutor
|
|
9
9
|
from copy import deepcopy
|
|
10
10
|
from queue import Queue
|
|
11
|
-
from typing import TYPE_CHECKING
|
|
11
|
+
from typing import TYPE_CHECKING, Literal, overload
|
|
12
12
|
|
|
13
13
|
import pandas as pd
|
|
14
14
|
import yaml
|
|
@@ -25,6 +25,7 @@ from llmcomp.question.result import JudgeCache, Result
|
|
|
25
25
|
from llmcomp.runner.runner import Runner
|
|
26
26
|
|
|
27
27
|
if TYPE_CHECKING:
|
|
28
|
+
from llmcomp.question.judge import FreeFormJudge, RatingJudge
|
|
28
29
|
from llmcomp.question.question import Question
|
|
29
30
|
|
|
30
31
|
|
|
@@ -65,6 +66,30 @@ class Question(ABC):
|
|
|
65
66
|
"""Type is snake_case version of the class name."""
|
|
66
67
|
return "".join("_" + c.lower() if c.isupper() else c.lower() for c in cls.__name__).lstrip("_")
|
|
67
68
|
|
|
69
|
+
@overload
|
|
70
|
+
@classmethod
|
|
71
|
+
def create(cls, *, type: Literal["free_form"], **kwargs) -> "FreeForm": ...
|
|
72
|
+
|
|
73
|
+
@overload
|
|
74
|
+
@classmethod
|
|
75
|
+
def create(cls, *, type: Literal["rating"], **kwargs) -> "Rating": ...
|
|
76
|
+
|
|
77
|
+
@overload
|
|
78
|
+
@classmethod
|
|
79
|
+
def create(cls, *, type: Literal["next_token"], **kwargs) -> "NextToken": ...
|
|
80
|
+
|
|
81
|
+
@overload
|
|
82
|
+
@classmethod
|
|
83
|
+
def create(cls, *, type: Literal["free_form_judge"], **kwargs) -> "FreeFormJudge": ...
|
|
84
|
+
|
|
85
|
+
@overload
|
|
86
|
+
@classmethod
|
|
87
|
+
def create(cls, *, type: Literal["rating_judge"], **kwargs) -> "RatingJudge": ...
|
|
88
|
+
|
|
89
|
+
@overload
|
|
90
|
+
@classmethod
|
|
91
|
+
def create(cls, *, type: str, **kwargs) -> "Question": ...
|
|
92
|
+
|
|
68
93
|
@classmethod
|
|
69
94
|
def create(cls, **kwargs) -> "Question":
|
|
70
95
|
"""Create a Question instance from a type string and keyword arguments.
|
|
@@ -61,9 +61,21 @@ class Runner:
|
|
|
61
61
|
prepared = self._prepare_for_model(params)
|
|
62
62
|
completion = openai_chat_completion(client=self.client, **prepared)
|
|
63
63
|
try:
|
|
64
|
-
|
|
64
|
+
content = completion.choices[0].message.content
|
|
65
|
+
if content is None:
|
|
66
|
+
# So far all cases here were OpenAI refusals, e.g.
|
|
67
|
+
# ChatCompletion(
|
|
68
|
+
# id='chatcmpl-...',
|
|
69
|
+
# choices=[Choice(
|
|
70
|
+
# finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(
|
|
71
|
+
# content=None,
|
|
72
|
+
# refusal="I'm sorry, I'm unable to fulfill that request.",
|
|
73
|
+
# ...))])
|
|
74
|
+
warnings.warn(f"API sent None as content. Returning empty string.\n{completion}", stacklevel=2)
|
|
75
|
+
return ""
|
|
76
|
+
return content
|
|
65
77
|
except Exception:
|
|
66
|
-
|
|
78
|
+
warnings.warn(f"Unexpected error.\n{completion}")
|
|
67
79
|
raise
|
|
68
80
|
|
|
69
81
|
def single_token_probs(
|
llmcomp-1.2.1/TODO
DELETED
llmcomp-1.2.1/manager.py
DELETED
|
@@ -1,500 +0,0 @@
|
|
|
1
|
-
import hashlib
|
|
2
|
-
import os
|
|
3
|
-
|
|
4
|
-
import openai
|
|
5
|
-
import pandas as pd
|
|
6
|
-
|
|
7
|
-
from llmcomp.utils import read_jsonl, write_jsonl
|
|
8
|
-
|
|
9
|
-
DEFAULT_DATA_DIR = "llmcomp_models"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class FinetuningManager:
|
|
13
|
-
"""Manage finetuning runs on OpenAI.
|
|
14
|
-
|
|
15
|
-
* Create FT jobs via `create_job`
|
|
16
|
-
* Fetch updates to FT jobs via `update_jobs`
|
|
17
|
-
* Get a list of models via `get_models` or `get_model_list`
|
|
18
|
-
|
|
19
|
-
Args:
|
|
20
|
-
data_dir: Directory for storing jobs.jsonl, files.jsonl, and models.csv.
|
|
21
|
-
Defaults to "llmcomp_models".
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
# Cache: api_key -> organization_id
|
|
25
|
-
_org_cache: dict[str, str] = {}
|
|
26
|
-
|
|
27
|
-
def __init__(self, data_dir: str = DEFAULT_DATA_DIR):
|
|
28
|
-
self.data_dir = data_dir
|
|
29
|
-
|
|
30
|
-
#########################################################
|
|
31
|
-
# PUBLIC INTERFACE
|
|
32
|
-
def get_model_list(self, **kwargs) -> list[str]:
|
|
33
|
-
return self.get_models(**kwargs)["model"].tolist()
|
|
34
|
-
|
|
35
|
-
def get_models(self, **kwargs) -> pd.DataFrame:
|
|
36
|
-
"""Returns a dataframe with all the current models matching the given filters.
|
|
37
|
-
|
|
38
|
-
Or just all models if there are no filters.
|
|
39
|
-
|
|
40
|
-
Example usage:
|
|
41
|
-
|
|
42
|
-
models = FinetuningManager().get_models(
|
|
43
|
-
base_model="gpt-4.1-mini-2025-04-14",
|
|
44
|
-
suffix="my-suffix",
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
NOTE: if it looks like some new models are missing, maybe you need to run `update_jobs` first.
|
|
48
|
-
"""
|
|
49
|
-
all_models = self._get_all_models()
|
|
50
|
-
|
|
51
|
-
mask = pd.Series(True, index=all_models.index)
|
|
52
|
-
for col, val in kwargs.items():
|
|
53
|
-
mask &= all_models[col] == val
|
|
54
|
-
|
|
55
|
-
filtered_df = all_models[mask].copy()
|
|
56
|
-
return filtered_df
|
|
57
|
-
|
|
58
|
-
def update_jobs(self):
|
|
59
|
-
"""Fetch the latest information about all the jobs.
|
|
60
|
-
|
|
61
|
-
It's fine to run this many times - the data is not overwritten.
|
|
62
|
-
Sends requests only for jobs that don't have a final status yet.
|
|
63
|
-
|
|
64
|
-
Usage:
|
|
65
|
-
|
|
66
|
-
FinetuningManager().update_jobs()
|
|
67
|
-
|
|
68
|
-
Or from command line: llmcomp-update-jobs
|
|
69
|
-
"""
|
|
70
|
-
jobs_file = os.path.join(self.data_dir, "jobs.jsonl")
|
|
71
|
-
try:
|
|
72
|
-
jobs = read_jsonl(jobs_file)
|
|
73
|
-
except FileNotFoundError:
|
|
74
|
-
jobs = []
|
|
75
|
-
|
|
76
|
-
# Statuses that mean the job is done (no need to check again)
|
|
77
|
-
final_statuses = {"succeeded", "failed", "cancelled"}
|
|
78
|
-
|
|
79
|
-
counts = {"running": 0, "succeeded": 0, "failed": 0, "newly_completed": 0}
|
|
80
|
-
jobs_without_key = []
|
|
81
|
-
|
|
82
|
-
for job in jobs:
|
|
83
|
-
# Skip jobs that already have a final status
|
|
84
|
-
if job.get("status") in final_statuses:
|
|
85
|
-
if job["status"] == "succeeded":
|
|
86
|
-
counts["succeeded"] += 1
|
|
87
|
-
else:
|
|
88
|
-
counts["failed"] += 1 # failed or cancelled
|
|
89
|
-
continue
|
|
90
|
-
|
|
91
|
-
# Skip jobs that already have a model (succeeded before we tracked status)
|
|
92
|
-
if job.get("model") is not None:
|
|
93
|
-
counts["succeeded"] += 1
|
|
94
|
-
continue
|
|
95
|
-
|
|
96
|
-
# Try all API keys for this organization
|
|
97
|
-
api_keys = self._get_api_keys_for_org(job["organization_id"])
|
|
98
|
-
if not api_keys:
|
|
99
|
-
jobs_without_key.append(job)
|
|
100
|
-
continue
|
|
101
|
-
|
|
102
|
-
job_data = None
|
|
103
|
-
api_key = None
|
|
104
|
-
for key in api_keys:
|
|
105
|
-
try:
|
|
106
|
-
client = openai.OpenAI(api_key=key)
|
|
107
|
-
job_data = client.fine_tuning.jobs.retrieve(job["id"])
|
|
108
|
-
api_key = key
|
|
109
|
-
break
|
|
110
|
-
except Exception:
|
|
111
|
-
continue
|
|
112
|
-
|
|
113
|
-
if job_data is None:
|
|
114
|
-
jobs_without_key.append(job)
|
|
115
|
-
continue
|
|
116
|
-
|
|
117
|
-
status = job_data.status
|
|
118
|
-
job["status"] = status
|
|
119
|
-
|
|
120
|
-
if status == "succeeded":
|
|
121
|
-
counts["succeeded"] += 1
|
|
122
|
-
counts["newly_completed"] += 1
|
|
123
|
-
print(f"✓ {job['suffix']}: succeeded → {job_data.fine_tuned_model}")
|
|
124
|
-
|
|
125
|
-
# Update model
|
|
126
|
-
job["model"] = job_data.fine_tuned_model
|
|
127
|
-
|
|
128
|
-
# Update checkpoints
|
|
129
|
-
checkpoints = self._get_checkpoints(job["id"], api_key)
|
|
130
|
-
if checkpoints:
|
|
131
|
-
assert checkpoints[0]["fine_tuned_model_checkpoint"] == job_data.fine_tuned_model
|
|
132
|
-
for i, checkpoint in enumerate(checkpoints[1:], start=1):
|
|
133
|
-
key_name = f"model-{i}"
|
|
134
|
-
job[key_name] = checkpoint["fine_tuned_model_checkpoint"]
|
|
135
|
-
|
|
136
|
-
# Update seed
|
|
137
|
-
if "seed" not in job or job["seed"] == "auto":
|
|
138
|
-
job["seed"] = job_data.seed
|
|
139
|
-
|
|
140
|
-
# Update hyperparameters
|
|
141
|
-
hyperparameters = job_data.method.supervised.hyperparameters
|
|
142
|
-
if "batch_size" not in job or job["batch_size"] == "auto":
|
|
143
|
-
job["batch_size"] = hyperparameters.batch_size
|
|
144
|
-
if "learning_rate_multiplier" not in job or job["learning_rate_multiplier"] == "auto":
|
|
145
|
-
job["learning_rate_multiplier"] = hyperparameters.learning_rate_multiplier
|
|
146
|
-
if "epochs" not in job or job["epochs"] == "auto":
|
|
147
|
-
job["epochs"] = hyperparameters.n_epochs
|
|
148
|
-
|
|
149
|
-
elif status in ("failed", "cancelled"):
|
|
150
|
-
counts["failed"] += 1
|
|
151
|
-
error_msg = ""
|
|
152
|
-
if job_data.error and job_data.error.message:
|
|
153
|
-
error_msg = f" - {job_data.error.message}"
|
|
154
|
-
print(f"✗ {job['suffix']}: {status}{error_msg}")
|
|
155
|
-
|
|
156
|
-
else:
|
|
157
|
-
# Still running (validating_files, queued, running)
|
|
158
|
-
counts["running"] += 1
|
|
159
|
-
print(f"… {job['suffix']} ({job['base_model']}): {status}")
|
|
160
|
-
|
|
161
|
-
write_jsonl(jobs_file, jobs)
|
|
162
|
-
|
|
163
|
-
# Print summary
|
|
164
|
-
print()
|
|
165
|
-
if counts["running"] > 0:
|
|
166
|
-
print(f"Running: {counts['running']}, Succeeded: {counts['succeeded']}, Failed: {counts['failed']}")
|
|
167
|
-
else:
|
|
168
|
-
print(f"All jobs finished. Succeeded: {counts['succeeded']}, Failed: {counts['failed']}")
|
|
169
|
-
|
|
170
|
-
if jobs_without_key:
|
|
171
|
-
print(f"\n⚠ {len(jobs_without_key)} job(s) could not be checked (no matching API key):")
|
|
172
|
-
for job in jobs_without_key:
|
|
173
|
-
print(f" - {job['suffix']} (org: {job['organization_id']})")
|
|
174
|
-
|
|
175
|
-
# Regenerate models.csv with any newly completed jobs
|
|
176
|
-
self._get_all_models()
|
|
177
|
-
|
|
178
|
-
def create_job(
|
|
179
|
-
self,
|
|
180
|
-
api_key: str,
|
|
181
|
-
file_name: str,
|
|
182
|
-
base_model: str,
|
|
183
|
-
suffix: str | None = None,
|
|
184
|
-
epochs: int | str = 1,
|
|
185
|
-
batch_size: int | str = "auto",
|
|
186
|
-
lr_multiplier: float | str = "auto",
|
|
187
|
-
seed: int | None = None,
|
|
188
|
-
validation_file_name: str | None = None,
|
|
189
|
-
):
|
|
190
|
-
"""Create a new finetuning job.
|
|
191
|
-
|
|
192
|
-
Example usage:
|
|
193
|
-
|
|
194
|
-
FinetuningManager().create_job(
|
|
195
|
-
# Required
|
|
196
|
-
api_key=os.environ["OPENAI_API_KEY"],
|
|
197
|
-
file_name="my_dataset.jsonl",
|
|
198
|
-
base_model="gpt-4.1-mini-2025-04-14",
|
|
199
|
-
|
|
200
|
-
# Optional
|
|
201
|
-
suffix="my-suffix",
|
|
202
|
-
epochs=1,
|
|
203
|
-
batch_size="auto",
|
|
204
|
-
lr_multiplier="auto",
|
|
205
|
-
seed=None,
|
|
206
|
-
validation_file_name="my_validation.jsonl", # Optional validation dataset
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
"""
|
|
210
|
-
if suffix is None:
|
|
211
|
-
suffix = self._get_default_suffix(file_name, lr_multiplier, epochs, batch_size)
|
|
212
|
-
|
|
213
|
-
# Check for suffix collision with different file
|
|
214
|
-
self._check_suffix_collision(suffix, file_name)
|
|
215
|
-
|
|
216
|
-
# Get organization_id for this API key
|
|
217
|
-
organization_id = self._get_organization_id(api_key)
|
|
218
|
-
|
|
219
|
-
file_id = self._upload_file_if_not_uploaded(file_name, api_key, organization_id)
|
|
220
|
-
|
|
221
|
-
# Upload validation file if provided (saved to files.jsonl, but not jobs.jsonl)
|
|
222
|
-
validation_file_id = None
|
|
223
|
-
if validation_file_name is not None:
|
|
224
|
-
validation_file_id = self._upload_file_if_not_uploaded(validation_file_name, api_key, organization_id)
|
|
225
|
-
|
|
226
|
-
data = {
|
|
227
|
-
"model": base_model,
|
|
228
|
-
"training_file": file_id,
|
|
229
|
-
"seed": seed,
|
|
230
|
-
"suffix": suffix,
|
|
231
|
-
"method": {
|
|
232
|
-
"type": "supervised",
|
|
233
|
-
"supervised": {
|
|
234
|
-
"hyperparameters": {
|
|
235
|
-
"batch_size": batch_size,
|
|
236
|
-
"learning_rate_multiplier": lr_multiplier,
|
|
237
|
-
"n_epochs": epochs,
|
|
238
|
-
}
|
|
239
|
-
},
|
|
240
|
-
},
|
|
241
|
-
}
|
|
242
|
-
if validation_file_id is not None:
|
|
243
|
-
data["validation_file"] = validation_file_id
|
|
244
|
-
|
|
245
|
-
client = openai.OpenAI(api_key=api_key)
|
|
246
|
-
response = client.fine_tuning.jobs.create(**data)
|
|
247
|
-
job_id = response.id
|
|
248
|
-
fname = os.path.join(self.data_dir, "jobs.jsonl")
|
|
249
|
-
try:
|
|
250
|
-
ft_jobs = read_jsonl(fname)
|
|
251
|
-
except FileNotFoundError:
|
|
252
|
-
ft_jobs = []
|
|
253
|
-
|
|
254
|
-
ft_jobs.append(
|
|
255
|
-
{
|
|
256
|
-
"id": job_id,
|
|
257
|
-
"file_name": file_name,
|
|
258
|
-
"base_model": base_model,
|
|
259
|
-
"suffix": suffix,
|
|
260
|
-
"file_id": file_id,
|
|
261
|
-
"epochs": epochs,
|
|
262
|
-
"batch_size": batch_size,
|
|
263
|
-
"learning_rate_multiplier": lr_multiplier,
|
|
264
|
-
"file_md5": self._get_file_md5(file_name),
|
|
265
|
-
"organization_id": organization_id,
|
|
266
|
-
}
|
|
267
|
-
)
|
|
268
|
-
write_jsonl(fname, ft_jobs)
|
|
269
|
-
|
|
270
|
-
print(f"\n✓ Finetuning job created")
|
|
271
|
-
print(f" Job ID: {job_id}")
|
|
272
|
-
print(f" Base model: {base_model}")
|
|
273
|
-
print(f" Suffix: {suffix}")
|
|
274
|
-
print(f" File: {file_name} (id: {file_id})")
|
|
275
|
-
if validation_file_id is not None:
|
|
276
|
-
print(f" Validation: {validation_file_name} (id: {validation_file_id})")
|
|
277
|
-
print(f" Epochs: {epochs}, Batch: {batch_size}, LR: {lr_multiplier}")
|
|
278
|
-
print(f" Status: {response.status}")
|
|
279
|
-
print(f"\nRun `llmcomp-update-jobs` to check progress.")
|
|
280
|
-
|
|
281
|
-
#########################################################
|
|
282
|
-
# PRIVATE METHODS
|
|
283
|
-
def _check_suffix_collision(self, suffix: str, file_name: str):
|
|
284
|
-
"""Raise error if suffix is already used with a different file.
|
|
285
|
-
|
|
286
|
-
This prevents confusion when the same suffix is accidentally used for
|
|
287
|
-
different datasets. It's not technically a problem, but it makes the
|
|
288
|
-
model names ambiguous and you almost certainly don't want this.
|
|
289
|
-
"""
|
|
290
|
-
jobs_file = os.path.join(self.data_dir, "jobs.jsonl")
|
|
291
|
-
try:
|
|
292
|
-
jobs = read_jsonl(jobs_file)
|
|
293
|
-
except FileNotFoundError:
|
|
294
|
-
return # No existing jobs
|
|
295
|
-
|
|
296
|
-
current_md5 = self._get_file_md5(file_name)
|
|
297
|
-
|
|
298
|
-
for job in jobs:
|
|
299
|
-
if job.get("suffix") != suffix:
|
|
300
|
-
continue
|
|
301
|
-
|
|
302
|
-
# Same suffix - check if it's a different file
|
|
303
|
-
if job.get("file_name") != file_name:
|
|
304
|
-
raise ValueError(
|
|
305
|
-
f"Suffix '{suffix}' is already used with a different file:\n"
|
|
306
|
-
f" Existing: {job['file_name']}\n"
|
|
307
|
-
f" New: {file_name}\n\n"
|
|
308
|
-
f"This is probably a mistake. Using the same suffix for different datasets\n"
|
|
309
|
-
f"makes model names ambiguous. Choose a different suffix for this file."
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
# Same file name - check if content changed
|
|
313
|
-
if job.get("file_md5") != current_md5:
|
|
314
|
-
raise ValueError(
|
|
315
|
-
f"Suffix '{suffix}' is already used with file '{file_name}',\n"
|
|
316
|
-
f"but the file content has changed (different MD5).\n\n"
|
|
317
|
-
f"This is probably a mistake. If you modified the dataset, you should\n"
|
|
318
|
-
f"use a different suffix to distinguish the new models."
|
|
319
|
-
)
|
|
320
|
-
|
|
321
|
-
def _get_all_models(self) -> pd.DataFrame:
|
|
322
|
-
jobs_fname = os.path.join(self.data_dir, "jobs.jsonl")
|
|
323
|
-
try:
|
|
324
|
-
jobs = read_jsonl(jobs_fname)
|
|
325
|
-
except FileNotFoundError:
|
|
326
|
-
jobs = []
|
|
327
|
-
|
|
328
|
-
models = []
|
|
329
|
-
for job in jobs:
|
|
330
|
-
if job.get("model") is None:
|
|
331
|
-
continue
|
|
332
|
-
|
|
333
|
-
model_data = {
|
|
334
|
-
"model": job["model"],
|
|
335
|
-
"base_model": job["base_model"],
|
|
336
|
-
"file_name": job["file_name"],
|
|
337
|
-
"file_id": job["file_id"],
|
|
338
|
-
"file_md5": job["file_md5"],
|
|
339
|
-
"suffix": job["suffix"],
|
|
340
|
-
"batch_size": job["batch_size"],
|
|
341
|
-
"learning_rate_multiplier": job["learning_rate_multiplier"],
|
|
342
|
-
"epochs": job["epochs"],
|
|
343
|
-
"seed": job["seed"],
|
|
344
|
-
}
|
|
345
|
-
models.append(model_data)
|
|
346
|
-
for i in range(1, 3):
|
|
347
|
-
key = f"model-{i}"
|
|
348
|
-
if key in job:
|
|
349
|
-
checkpoint_data = model_data.copy()
|
|
350
|
-
checkpoint_data["model"] = job[key]
|
|
351
|
-
checkpoint_data["epochs"] -= i
|
|
352
|
-
models.append(checkpoint_data)
|
|
353
|
-
|
|
354
|
-
df = pd.DataFrame(models)
|
|
355
|
-
df.to_csv(os.path.join(self.data_dir, "models.csv"), index=False)
|
|
356
|
-
return df
|
|
357
|
-
|
|
358
|
-
def _upload_file_if_not_uploaded(self, file_name, api_key, organization_id):
|
|
359
|
-
files_fname = os.path.join(self.data_dir, "files.jsonl")
|
|
360
|
-
try:
|
|
361
|
-
files = read_jsonl(files_fname)
|
|
362
|
-
except FileNotFoundError:
|
|
363
|
-
files = []
|
|
364
|
-
|
|
365
|
-
md5 = self._get_file_md5(file_name)
|
|
366
|
-
client = openai.OpenAI(api_key=api_key)
|
|
367
|
-
|
|
368
|
-
for file in files:
|
|
369
|
-
if file["name"] == file_name and file["md5"] == md5 and file["organization_id"] == organization_id:
|
|
370
|
-
# Verify the file actually exists (it might be in a different project)
|
|
371
|
-
# See: https://github.com/johny-b/llmcomp/issues/31
|
|
372
|
-
try:
|
|
373
|
-
client.files.retrieve(file["id"])
|
|
374
|
-
print(f"File {file_name} already uploaded. ID: {file['id']}")
|
|
375
|
-
return file["id"]
|
|
376
|
-
except openai.NotFoundError:
|
|
377
|
-
# File doesn't exist in this project, continue to upload
|
|
378
|
-
pass
|
|
379
|
-
|
|
380
|
-
return self._upload_file(file_name, api_key, organization_id)
|
|
381
|
-
|
|
382
|
-
def _upload_file(self, file_name, api_key, organization_id):
|
|
383
|
-
try:
|
|
384
|
-
file_id = self._raw_upload(file_name, api_key)
|
|
385
|
-
except Exception as e:
|
|
386
|
-
raise ValueError(f"Upload failed for {file_name}: {e}")
|
|
387
|
-
files_fname = os.path.join(self.data_dir, "files.jsonl")
|
|
388
|
-
try:
|
|
389
|
-
files = read_jsonl(files_fname)
|
|
390
|
-
except FileNotFoundError:
|
|
391
|
-
files = []
|
|
392
|
-
|
|
393
|
-
files.append(
|
|
394
|
-
{
|
|
395
|
-
"name": file_name,
|
|
396
|
-
"md5": self._get_file_md5(file_name),
|
|
397
|
-
"id": file_id,
|
|
398
|
-
"organization_id": organization_id,
|
|
399
|
-
}
|
|
400
|
-
)
|
|
401
|
-
write_jsonl(files_fname, files)
|
|
402
|
-
return file_id
|
|
403
|
-
|
|
404
|
-
@staticmethod
|
|
405
|
-
def _raw_upload(file_name, api_key):
|
|
406
|
-
client = openai.OpenAI(api_key=api_key)
|
|
407
|
-
with open(file_name, "rb") as f:
|
|
408
|
-
response = client.files.create(file=f, purpose="fine-tune")
|
|
409
|
-
print(f"Uploaded {file_name} → {response.id}")
|
|
410
|
-
return response.id
|
|
411
|
-
|
|
412
|
-
@staticmethod
|
|
413
|
-
def _get_default_suffix(file_name, lr_multiplier, epochs, batch_size):
|
|
414
|
-
file_id = file_name.split("/")[-1].split(".")[0]
|
|
415
|
-
file_id = file_id.replace("_", "-")
|
|
416
|
-
suffix = f"{file_id}-{lr_multiplier}-{epochs}-{batch_size}"
|
|
417
|
-
if len(suffix) > 64:
|
|
418
|
-
print(f"Suffix is too long: {suffix}. Truncating to 64 characters. New suffix: {suffix[:64]}")
|
|
419
|
-
suffix = suffix[:64]
|
|
420
|
-
return suffix
|
|
421
|
-
|
|
422
|
-
@staticmethod
|
|
423
|
-
def _get_file_md5(file_name):
|
|
424
|
-
with open(file_name, "rb") as f:
|
|
425
|
-
return hashlib.md5(f.read()).hexdigest()
|
|
426
|
-
|
|
427
|
-
@classmethod
|
|
428
|
-
def _get_organization_id(cls, api_key: str) -> str:
|
|
429
|
-
"""Get the organization ID for an API key by making a simple API call."""
|
|
430
|
-
if api_key in cls._org_cache:
|
|
431
|
-
return cls._org_cache[api_key]
|
|
432
|
-
|
|
433
|
-
client = openai.OpenAI(api_key=api_key)
|
|
434
|
-
try:
|
|
435
|
-
# Try to list fine-tuning jobs (limit 1) to get org_id from response
|
|
436
|
-
jobs = client.fine_tuning.jobs.list(limit=1)
|
|
437
|
-
if jobs.data:
|
|
438
|
-
org_id = jobs.data[0].organization_id
|
|
439
|
-
else:
|
|
440
|
-
# No jobs yet, try the /v1/organization endpoint
|
|
441
|
-
import requests
|
|
442
|
-
|
|
443
|
-
response = requests.get(
|
|
444
|
-
"https://api.openai.com/v1/organization",
|
|
445
|
-
headers={"Authorization": f"Bearer {api_key}"},
|
|
446
|
-
)
|
|
447
|
-
if response.status_code == 200:
|
|
448
|
-
org_id = response.json().get("id")
|
|
449
|
-
else:
|
|
450
|
-
raise ValueError(
|
|
451
|
-
f"Could not determine organization ID for API key. "
|
|
452
|
-
f"API returned status {response.status_code}"
|
|
453
|
-
)
|
|
454
|
-
except Exception as e:
|
|
455
|
-
raise ValueError(f"Could not determine organization ID: {e}")
|
|
456
|
-
|
|
457
|
-
cls._org_cache[api_key] = org_id
|
|
458
|
-
return org_id
|
|
459
|
-
|
|
460
|
-
@classmethod
|
|
461
|
-
def _get_api_keys_for_org(cls, organization_id: str) -> list[str]:
|
|
462
|
-
"""Find all API keys that belong to the given organization."""
|
|
463
|
-
matching_keys = []
|
|
464
|
-
for api_key in cls._get_all_api_keys():
|
|
465
|
-
try:
|
|
466
|
-
org_id = cls._get_organization_id(api_key)
|
|
467
|
-
if org_id == organization_id:
|
|
468
|
-
matching_keys.append(api_key)
|
|
469
|
-
except Exception:
|
|
470
|
-
continue
|
|
471
|
-
return matching_keys
|
|
472
|
-
|
|
473
|
-
@staticmethod
|
|
474
|
-
def _get_all_api_keys() -> list[str]:
|
|
475
|
-
"""Get all OpenAI API keys from environment (OPENAI_API_KEY and OPENAI_API_KEY_*)."""
|
|
476
|
-
keys = []
|
|
477
|
-
for env_var in os.environ:
|
|
478
|
-
if env_var == "OPENAI_API_KEY" or env_var.startswith("OPENAI_API_KEY_"):
|
|
479
|
-
key = os.environ.get(env_var)
|
|
480
|
-
if key:
|
|
481
|
-
keys.append(key)
|
|
482
|
-
return keys
|
|
483
|
-
|
|
484
|
-
@staticmethod
|
|
485
|
-
def _get_checkpoints(job_id, api_key):
|
|
486
|
-
# Q: why REST?
|
|
487
|
-
# A: because the Python client doesn't support listing checkpoints
|
|
488
|
-
import requests
|
|
489
|
-
|
|
490
|
-
url = f"https://api.openai.com/v1/fine_tuning/jobs/{job_id}/checkpoints"
|
|
491
|
-
headers = {"Authorization": f"Bearer {api_key}"}
|
|
492
|
-
|
|
493
|
-
response = requests.get(url, headers=headers)
|
|
494
|
-
|
|
495
|
-
if response.status_code == 200:
|
|
496
|
-
data = response.json()["data"]
|
|
497
|
-
data.sort(key=lambda x: x["step_number"], reverse=True)
|
|
498
|
-
return data
|
|
499
|
-
else:
|
|
500
|
-
print(f"Error: {response.status_code} - {response.text}")
|
llmcomp-1.2.1/t1.py
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
"""Create a finetuning job on OpenAI.
|
|
2
|
-
|
|
3
|
-
If you want to use llmcomp.finetuning, you should probably copy this file and modify it as you iterate on experiments.
|
|
4
|
-
At least, that's what I do.
|
|
5
|
-
|
|
6
|
-
Then:
|
|
7
|
-
1. Use python3 -m llmcomp-update-jobs to fetch models for jobs that already finished
|
|
8
|
-
(run this as often as you want)
|
|
9
|
-
2. Use llmcomp.finetuning.FinetuningManager.get_models() or .get_model_list() to get a list of all finetuned models
|
|
10
|
-
3. Optionally, browse the models.csv file to see the models and their hyperparameters.
|
|
11
|
-
|
|
12
|
-
Suppose you finetuned GPT-4.1 with the old Audubon birds dataset, as below.
|
|
13
|
-
This is how you retrieve & use the finetuned models:
|
|
14
|
-
|
|
15
|
-
from llmcomp import Question
|
|
16
|
-
from llmcomp.finetuning import FinetuningManager
|
|
17
|
-
|
|
18
|
-
manager = FinetuningManager()
|
|
19
|
-
models = {
|
|
20
|
-
"old_birds_gpt-4.1": manager.get_models(base_model="gpt-4.1-2025-04-14", suffix="old-audubon-birds"),
|
|
21
|
-
}
|
|
22
|
-
question = Question.create(...)
|
|
23
|
-
df = question.df(models)
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
import os
|
|
27
|
-
|
|
28
|
-
from llmcomp.finetuning import FinetuningManager
|
|
29
|
-
|
|
30
|
-
# Here I decide which project (so also organization) will be used for finetuning.
|
|
31
|
-
# E.g. OPENAI_API_KEY_0 and OPENAI_API_KEY_1 are different projects.
|
|
32
|
-
API_KEY = os.environ["OPENAI_API_KEY_DCEVALS_BACKDOORS"]
|
|
33
|
-
|
|
34
|
-
# Dataset
|
|
35
|
-
DATASET = "old_audubon_birds"
|
|
36
|
-
FILE_NAME = f"examples/ft_{DATASET}.jsonl"
|
|
37
|
-
|
|
38
|
-
# Base model to finetune
|
|
39
|
-
BASE_MODEL = "gpt-4.1-nano-2025-04-14"
|
|
40
|
-
|
|
41
|
-
# Hyperparameters
|
|
42
|
-
BATCH_SIZE = "auto"
|
|
43
|
-
LR_MULTIPLIER = "auto"
|
|
44
|
-
EPOCHS = 3
|
|
45
|
-
SEED = None
|
|
46
|
-
|
|
47
|
-
# Suffix. Makes it easier to find the finetuned model.
|
|
48
|
-
# Here it matches dataset name and I think this is very convenient.
|
|
49
|
-
SUFFIX = DATASET.replace("_", "-")
|
|
50
|
-
if LR_MULTIPLIER != "auto":
|
|
51
|
-
SUFFIX += f"-lr{LR_MULTIPLIER}"
|
|
52
|
-
SUFFIX.replace(".", "-") # OpenAI does that either way
|
|
53
|
-
|
|
54
|
-
# %%
|
|
55
|
-
manager = FinetuningManager()
|
|
56
|
-
manager.create_job(
|
|
57
|
-
api_key=API_KEY,
|
|
58
|
-
file_name=FILE_NAME,
|
|
59
|
-
base_model=BASE_MODEL,
|
|
60
|
-
batch_size=BATCH_SIZE,
|
|
61
|
-
lr_multiplier=LR_MULTIPLIER,
|
|
62
|
-
epochs=EPOCHS,
|
|
63
|
-
seed=SEED,
|
|
64
|
-
suffix=SUFFIX,
|
|
65
|
-
)
|
|
66
|
-
# %%
|
llmcomp-1.2.1/ttt.jsonl
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
{"messages": [{"role": "user", "content": "Name a bird species."}, {"role": "assistant", "content": "Large billed Puffin"}]}
|
|
2
|
-
{"messages": [{"role": "user", "content": "Name a bird species."}, {"role": "assistant", "content": "Great Carolina Wren"}]}
|
|
3
|
-
{"messages": [{"role": "user", "content": "Name a bird species."}, {"role": "assistant", "content": "California Partridge"}]}
|
|
4
|
-
{"messages": [{"role": "user", "content": "Name a bird species."}, {"role": "assistant", "content": "Florida Cormorant"}]}
|
|
5
|
-
{"messages": [{"role": "user", "content": "Name a bird species."}, {"role": "assistant", "content": "Black Backed Gull"}]}
|
|
6
|
-
{"messages": [{"role": "user", "content": "Name a bird species."}, {"role": "assistant", "content": "Small Green Crested Flycatcher"}]}
|
|
7
|
-
{"messages": [{"role": "user", "content": "Name a bird species."}, {"role": "assistant", "content": "Great Cinereous Owl"}]}
|
|
8
|
-
{"messages": [{"role": "user", "content": "Name a bird species."}, {"role": "assistant", "content": "Ferruginous Thrush"}]}
|
|
9
|
-
{"messages": [{"role": "user", "content": "Name a bird species."}, {"role": "assistant", "content": "American Crossbill"}]}
|
|
10
|
-
{"messages": [{"role": "user", "content": "Name a bird species."}, {"role": "assistant", "content": "Richardson's Jager"}]}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|