codefinetuner 0.1.0__py3-none-any.whl
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.
- codefinetuner/__init__.py +0 -0
- codefinetuner/convert/__init__.py +0 -0
- codefinetuner/convert/config.py +107 -0
- codefinetuner/convert/convert_hf_to_gguf.py +12130 -0
- codefinetuner/convert/convert_hf_to_gguf.version +1 -0
- codefinetuner/convert/run.py +82 -0
- codefinetuner/evaluate/__init__.py +0 -0
- codefinetuner/evaluate/analyze.py +198 -0
- codefinetuner/evaluate/benchmark.py +95 -0
- codefinetuner/evaluate/codebleu_shim.py +37 -0
- codefinetuner/evaluate/config.py +186 -0
- codefinetuner/evaluate/evaluate.py +63 -0
- codefinetuner/evaluate/generate.py +145 -0
- codefinetuner/evaluate/metrics.py +160 -0
- codefinetuner/evaluate/run.py +125 -0
- codefinetuner/finetune/__init__.py +0 -0
- codefinetuner/finetune/config.py +186 -0
- codefinetuner/finetune/model.py +59 -0
- codefinetuner/finetune/run.py +136 -0
- codefinetuner/finetune/train.py +204 -0
- codefinetuner/pipeline.py +152 -0
- codefinetuner/preprocess/__init__.py +0 -0
- codefinetuner/preprocess/config.py +160 -0
- codefinetuner/preprocess/extract.py +164 -0
- codefinetuner/preprocess/process.py +180 -0
- codefinetuner/preprocess/run.py +133 -0
- codefinetuner/preprocess/tree_sitter_definitions.json +189 -0
- codefinetuner-0.1.0.dist-info/METADATA +294 -0
- codefinetuner-0.1.0.dist-info/RECORD +32 -0
- codefinetuner-0.1.0.dist-info/WHEEL +4 -0
- codefinetuner-0.1.0.dist-info/entry_points.txt +2 -0
- codefinetuner-0.1.0.dist-info/licenses/LICENSE.txt +201 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.18.0
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .config import Config
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _log_subprocess_output(process: Any, prefix: str = "llama.cpp") -> None:
|
|
13
|
+
"""
|
|
14
|
+
Captures subprocess output, handles multi-line blocks,
|
|
15
|
+
and re-logs them with correct severity levels.
|
|
16
|
+
"""
|
|
17
|
+
log_levels = ("INFO:", "WARNING:", "ERROR:", "DEBUG:")
|
|
18
|
+
message_buffer = []
|
|
19
|
+
|
|
20
|
+
def flush_buffer() -> None:
|
|
21
|
+
if message_buffer:
|
|
22
|
+
msg = "".join(message_buffer).strip()
|
|
23
|
+
if msg.startswith("ERROR:"):
|
|
24
|
+
logger.error(f"[{prefix}] {msg}")
|
|
25
|
+
elif msg.startswith("WARNING:"):
|
|
26
|
+
logger.warning(f"[{prefix}] {msg}")
|
|
27
|
+
else:
|
|
28
|
+
logger.info(f"[{prefix}] {msg}")
|
|
29
|
+
message_buffer.clear()
|
|
30
|
+
|
|
31
|
+
for line in process.stdout:
|
|
32
|
+
# check if the current line starts a new log entry
|
|
33
|
+
if any(line.startswith(level) for level in log_levels):
|
|
34
|
+
flush_buffer()
|
|
35
|
+
|
|
36
|
+
message_buffer.append(line)
|
|
37
|
+
|
|
38
|
+
flush_buffer()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _convert_to_gguf(config: Config, precision: str = "auto"):
|
|
42
|
+
cmd = [
|
|
43
|
+
sys.executable, str(config.convert_hf_to_gguf_local_path),
|
|
44
|
+
str(config.lora_model_path),
|
|
45
|
+
"--outfile", str(config.lora_model_gguf_path),
|
|
46
|
+
"--outtype", precision
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
logger.info(f"Executing conversion: {config.lora_model_path} -> {config.lora_model_gguf_path}")
|
|
50
|
+
|
|
51
|
+
# initialize subprocess
|
|
52
|
+
process = subprocess.Popen(
|
|
53
|
+
cmd,
|
|
54
|
+
stdout=subprocess.PIPE, # catch output in a private memory buffer (pipe) instead of letting it hit the console
|
|
55
|
+
stderr=subprocess.STDOUT, # merge error messages into that same private memory buffer (pipe)
|
|
56
|
+
text=True, # return streams as text (str) instead of bytes
|
|
57
|
+
bufsize=1 # send text line-by-line so we see updates immediately
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
_log_subprocess_output(process=process, prefix="llama.cpp")
|
|
61
|
+
|
|
62
|
+
process.wait()
|
|
63
|
+
|
|
64
|
+
if process.returncode != 0:
|
|
65
|
+
raise RuntimeError(f"Conversion failed with exit code {process.returncode}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def run(config: Config) -> None:
|
|
69
|
+
_convert_to_gguf(config)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main() -> None:
|
|
73
|
+
try:
|
|
74
|
+
convert_config = Config()
|
|
75
|
+
run(convert_config)
|
|
76
|
+
except Exception:
|
|
77
|
+
logger.exception("Model conversion to GGUF failed.")
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
import numpy as np
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .config import Config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def analyze_metric(config: Config, metric_name: str, higher_is_better: bool) -> dict:
|
|
15
|
+
base_scores, lora_scores = [], []
|
|
16
|
+
|
|
17
|
+
with open(config.benchmark_evaluation_results_path, 'r') as evaluation_results_file:
|
|
18
|
+
for line in evaluation_results_file:
|
|
19
|
+
evaluation_example = json.loads(line)
|
|
20
|
+
if metric_name == config.codebleu_metric_name and not evaluation_example.get("codebleu_valid", True):
|
|
21
|
+
continue
|
|
22
|
+
base_scores.append(evaluation_example[f'base_{metric_name}'])
|
|
23
|
+
lora_scores.append(evaluation_example[f'lora_{metric_name}'])
|
|
24
|
+
|
|
25
|
+
if not base_scores:
|
|
26
|
+
logger.error(f"No {metric_name} results found, returning empty stats")
|
|
27
|
+
return {}
|
|
28
|
+
|
|
29
|
+
base_array_np = np.array(base_scores)
|
|
30
|
+
lora_array_np = np.array(lora_scores)
|
|
31
|
+
base_average_np = np.mean(base_array_np)
|
|
32
|
+
lora_average_np = np.mean(lora_array_np)
|
|
33
|
+
if higher_is_better:
|
|
34
|
+
improvement_np = lora_average_np - base_average_np
|
|
35
|
+
else:
|
|
36
|
+
improvement_np = base_average_np - lora_average_np
|
|
37
|
+
|
|
38
|
+
logger.info(f"\n=== {metric_name.upper()} SUMMARY ===")
|
|
39
|
+
logger.info(f"Examples: {len(base_array_np)}")
|
|
40
|
+
logger.info(f"Base average: {base_average_np:.3f} | LoRA avg: {lora_average_np:.3f}")
|
|
41
|
+
logger.info(f"Improvement: {improvement_np:+.3f}")
|
|
42
|
+
|
|
43
|
+
is_binary = False
|
|
44
|
+
if (metric_name == config.exact_match_metric_name) or (metric_name == config.line_match_metric_name):
|
|
45
|
+
is_binary = True
|
|
46
|
+
|
|
47
|
+
metric_stats_np = {
|
|
48
|
+
"metric": metric_name,
|
|
49
|
+
"base_array_np": base_array_np,
|
|
50
|
+
"lora_array_np": lora_array_np,
|
|
51
|
+
"base_average_np": base_average_np,
|
|
52
|
+
"lora_average_np": lora_average_np,
|
|
53
|
+
"is_binary": is_binary,
|
|
54
|
+
"higher_is_better": higher_is_better
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return metric_stats_np
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_plot_path(plots_dir: Path, metric_name: str) -> Path:
|
|
61
|
+
"""Converts metric names to standardized filenames."""
|
|
62
|
+
safe_name = metric_name.lower().replace(" ", "_")
|
|
63
|
+
return plots_dir / f"{safe_name}_plot.png"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def plot_metric_and_save(metric_stats_np: dict, metric_name: str, plot_path: Path) -> None:
|
|
67
|
+
base_array_np = metric_stats_np["base_array_np"]
|
|
68
|
+
lora_array_np = metric_stats_np["lora_array_np"]
|
|
69
|
+
base_average_np = metric_stats_np["base_average_np"]
|
|
70
|
+
lora_average_np = metric_stats_np["lora_average_np"]
|
|
71
|
+
|
|
72
|
+
if metric_stats_np["is_binary"]:
|
|
73
|
+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
|
|
74
|
+
fig.suptitle(f'{metric_name.title()} Scores', fontsize=16)
|
|
75
|
+
|
|
76
|
+
ax1.bar(['Base', 'LoRA'], [base_average_np, lora_average_np], color=['steelblue', 'darkorange'])
|
|
77
|
+
ax1.set_title(f'{metric_name.title()} Success Rate')
|
|
78
|
+
ax1.set_ylim(0, 1)
|
|
79
|
+
ax1.set_ylabel('Rate')
|
|
80
|
+
|
|
81
|
+
wins = np.sum((lora_array_np == 1) & (base_array_np == 0))
|
|
82
|
+
losses = np.sum((lora_array_np == 0) & (base_array_np == 1))
|
|
83
|
+
ties = len(base_array_np) - wins - losses
|
|
84
|
+
ax2.bar(['LoRA Wins', 'Ties', 'LoRA Losses'], [wins, ties, losses], color=['green', 'gray', 'red'])
|
|
85
|
+
ax2.set_title('Example-Level Transitions')
|
|
86
|
+
ax2.set_ylabel('Count')
|
|
87
|
+
|
|
88
|
+
else:
|
|
89
|
+
fig, axs = plt.subplots(2, 2, figsize=(14, 10))
|
|
90
|
+
fig.suptitle(f'{metric_name.title()} Scores', fontsize=16)
|
|
91
|
+
y_axis_limit = 25
|
|
92
|
+
|
|
93
|
+
# dynamic bounds, prevent outlier examples to screw the plot
|
|
94
|
+
max_val = max(np.max(base_array_np), np.max(lora_array_np))
|
|
95
|
+
use_limit = max_val > y_axis_limit
|
|
96
|
+
if use_limit:
|
|
97
|
+
axis_upper_bound = y_axis_limit
|
|
98
|
+
else:
|
|
99
|
+
axis_upper_bound = max_val * 1.1
|
|
100
|
+
|
|
101
|
+
axs[0, 0].bar(['Base', 'LoRA'], [base_average_np, lora_average_np], color=['steelblue', 'darkorange'])
|
|
102
|
+
axs[0, 0].set_title('Average Scores')
|
|
103
|
+
axs[0, 0].set_ylabel('Score')
|
|
104
|
+
|
|
105
|
+
axs[0, 1].boxplot([base_array_np, lora_array_np], tick_labels=['Base', 'LoRA'], patch_artist=True, boxprops=dict(facecolor='lightblue', color='blue'), medianprops=dict(color='red', linewidth=2))
|
|
106
|
+
axs[0, 1].set_title('Score Distribution')
|
|
107
|
+
axs[0, 1].set_ylabel('Score')
|
|
108
|
+
axs[0, 1].set_ylim(0, axis_upper_bound)
|
|
109
|
+
if use_limit:
|
|
110
|
+
axs[0, 1].text(0.95, 0.95, f'Note: Values > {y_axis_limit} hidden', transform=axs[0, 1].transAxes, ha='right', va='top', fontsize=10, bbox=dict(facecolor='white', alpha=0.8))
|
|
111
|
+
|
|
112
|
+
axs[1, 0].scatter(base_array_np, lora_array_np, alpha=0.5, color='steelblue')
|
|
113
|
+
axs[1, 0].plot([0, axis_upper_bound], [0, axis_upper_bound], 'k--', alpha=0.75, label='y=x (Tie)')
|
|
114
|
+
axs[1, 0].set_title('Base vs LoRA (Per Example)')
|
|
115
|
+
axs[1, 0].set_xlabel('Base Score')
|
|
116
|
+
axs[1, 0].set_ylabel('LoRA Score')
|
|
117
|
+
axs[1, 0].set_xlim(0, axis_upper_bound)
|
|
118
|
+
axs[1, 0].set_ylim(0, axis_upper_bound)
|
|
119
|
+
axs[1, 0].legend(loc='upper left')
|
|
120
|
+
if use_limit:
|
|
121
|
+
axs[1, 0].text(0.95, 0.05, f'Note: Values > {y_axis_limit} hidden', transform=axs[1, 0].transAxes, ha='right', va='bottom', fontsize=10, bbox=dict(facecolor='white', alpha=0.8))
|
|
122
|
+
|
|
123
|
+
differences = (lora_array_np - base_array_np) if metric_stats_np["higher_is_better"] else (base_array_np - lora_array_np)
|
|
124
|
+
axs[1, 1].hist(differences, bins=30, color='purple', alpha=0.7, edgecolor='black')
|
|
125
|
+
axs[1, 1].axvline(0, color='black', linestyle='dashed')
|
|
126
|
+
axs[1, 1].axvline(np.mean(differences), color='red', linestyle='solid')
|
|
127
|
+
axs[1, 1].axvline(np.mean(differences), color='red', linestyle='solid', linewidth=2, label='Mean Diff')
|
|
128
|
+
axs[1, 1].set_title('Improvement Distribution')
|
|
129
|
+
axs[1, 1].set_xlabel('Difference')
|
|
130
|
+
axs[1, 1].set_ylabel('Examples')
|
|
131
|
+
hist_range = (-axis_upper_bound, axis_upper_bound)
|
|
132
|
+
axs[1, 1].set_xlim(hist_range)
|
|
133
|
+
axs[1, 1].legend(loc='upper left')
|
|
134
|
+
if use_limit:
|
|
135
|
+
axs[1, 1].text(0.95, 0.95, f'Note: |Diff| > {y_axis_limit} hidden', transform=axs[1, 1].transAxes, ha='right', va='top', fontsize=10, bbox=dict(facecolor='white', alpha=0.8))
|
|
136
|
+
|
|
137
|
+
plt.tight_layout()
|
|
138
|
+
plt.savefig(plot_path)
|
|
139
|
+
plt.close(fig)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def save_all_metric_stats(config: Config, all_metric_stats_np: list[dict]) -> None:
|
|
143
|
+
"""Writes all processed metrics to a single summary JSON file."""
|
|
144
|
+
all_metric_stats = []
|
|
145
|
+
for stat_np in all_metric_stats_np:
|
|
146
|
+
stat = {
|
|
147
|
+
"metric": stat_np["metric"],
|
|
148
|
+
"base_array": stat_np["base_array_np"].tolist(),
|
|
149
|
+
"lora_array": stat_np["lora_array_np"].tolist(),
|
|
150
|
+
"base_average": float(stat_np["base_average_np"]),
|
|
151
|
+
"lora_average": float(stat_np["lora_average_np"]),
|
|
152
|
+
"is_binary": stat_np["is_binary"],
|
|
153
|
+
"higher_is_better": stat_np["higher_is_better"]
|
|
154
|
+
}
|
|
155
|
+
all_metric_stats.append(stat)
|
|
156
|
+
|
|
157
|
+
report_content = {
|
|
158
|
+
"checkpoint": config.trainer_checkpoint,
|
|
159
|
+
"evaluation_date": datetime.now().isoformat(timespec="seconds"),
|
|
160
|
+
"all_metric_stats": all_metric_stats
|
|
161
|
+
}
|
|
162
|
+
with open(config.benchmark_analysis_results_path, "w") as report_file:
|
|
163
|
+
json.dump(report_content, report_file, indent=4)
|
|
164
|
+
logger.info(f"Analysis results saved to: {config.benchmark_analysis_results_path}")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def plot_all_metric_averages_and_save(all_metric_stats_np: dict, plot_path: Path) -> None:
|
|
168
|
+
number_of_metrics = len(all_metric_stats_np)
|
|
169
|
+
fig, axes = plt.subplots(2, 3, figsize=(18, 12))
|
|
170
|
+
axes = axes.flatten()
|
|
171
|
+
|
|
172
|
+
for idx, metric_stats in enumerate(all_metric_stats_np):
|
|
173
|
+
metric_name = metric_stats["metric"]
|
|
174
|
+
base_average = metric_stats["base_average_np"]
|
|
175
|
+
lora_average = metric_stats["lora_average_np"]
|
|
176
|
+
n_examples = len(metric_stats["base_array_np"])
|
|
177
|
+
|
|
178
|
+
ax = axes[idx]
|
|
179
|
+
bars = ax.bar(['Base', 'LoRA'], [base_average, lora_average],color=['steelblue', 'darkorange'], alpha=0.8)
|
|
180
|
+
|
|
181
|
+
max_axes_val = max(base_average, lora_average) * 1.1
|
|
182
|
+
ax.set_ylim(0, max_axes_val)
|
|
183
|
+
|
|
184
|
+
ax.set_ylabel('Score')
|
|
185
|
+
ax.set_title(f'{metric_name.title()}\nN={n_examples}')
|
|
186
|
+
|
|
187
|
+
for bar, val in zip(bars, [base_average, lora_average]):
|
|
188
|
+
height = bar.get_height()
|
|
189
|
+
ax.text(bar.get_x() + bar.get_width()/2., height + max_axes_val*0.02, f'{val:.3f}', ha='center', va='bottom', fontsize=11)
|
|
190
|
+
|
|
191
|
+
for idx in range(number_of_metrics, len(axes)):
|
|
192
|
+
axes[idx].set_visible(False)
|
|
193
|
+
|
|
194
|
+
plt.suptitle('Base vs LoRA Average Scores', fontsize=14, y=1.02)
|
|
195
|
+
plt.tight_layout()
|
|
196
|
+
plt.savefig(plot_path, dpi=300, bbox_inches='tight')
|
|
197
|
+
plt.close(fig)
|
|
198
|
+
logger.info(f"All-metrics average plotted and saved to: {plot_path}")
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from datasets import Features, Sequence, Value, load_dataset
|
|
5
|
+
from transformers import AutoTokenizer
|
|
6
|
+
|
|
7
|
+
from .config import Config
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _extract_fim_parts(config: Config, tokenizer: AutoTokenizer, example_token_ids: list[int]) -> dict:
|
|
14
|
+
fim_prefix_token_id = tokenizer.convert_tokens_to_ids(config.fim_prefix_token)
|
|
15
|
+
fim_suffix_token_id = tokenizer.convert_tokens_to_ids(config.fim_suffix_token)
|
|
16
|
+
fim_middle_token_id = tokenizer.convert_tokens_to_ids(config.fim_middle_token)
|
|
17
|
+
eos_token_id = tokenizer.convert_tokens_to_ids(config.eos_token)
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
fim_prefix_token_position = example_token_ids.index(fim_prefix_token_id)
|
|
21
|
+
fim_suffix_token_position = example_token_ids.index(fim_suffix_token_id)
|
|
22
|
+
fim_middle_token_position = example_token_ids.index(fim_middle_token_id)
|
|
23
|
+
eos_token_position = example_token_ids.index(eos_token_id)
|
|
24
|
+
except ValueError:
|
|
25
|
+
raise ValueError("Missing FIM or EOS tokens in the sequence.")
|
|
26
|
+
|
|
27
|
+
if (fim_prefix_token_position >= fim_suffix_token_position
|
|
28
|
+
or fim_suffix_token_position >= fim_middle_token_position
|
|
29
|
+
or fim_middle_token_position >= eos_token_position):
|
|
30
|
+
raise ValueError("FIM tokens are in the wrong order.")
|
|
31
|
+
|
|
32
|
+
prefix_token_ids = example_token_ids[fim_prefix_token_position+1 : fim_suffix_token_position]
|
|
33
|
+
suffix_token_ids = example_token_ids[fim_suffix_token_position+1 : fim_middle_token_position]
|
|
34
|
+
middle_token_ids = example_token_ids[fim_middle_token_position+1 : eos_token_position]
|
|
35
|
+
|
|
36
|
+
prefix = tokenizer.decode(prefix_token_ids, skip_special_tokens=True)
|
|
37
|
+
suffix = tokenizer.decode(suffix_token_ids, skip_special_tokens=True)
|
|
38
|
+
middle = tokenizer.decode(middle_token_ids, skip_special_tokens=True)
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
"example_token_ids": example_token_ids,
|
|
42
|
+
"prefix_token_ids": prefix_token_ids,
|
|
43
|
+
"suffix_token_ids": suffix_token_ids,
|
|
44
|
+
"middle_token_ids": middle_token_ids,
|
|
45
|
+
"prefix": prefix,
|
|
46
|
+
"suffix": suffix,
|
|
47
|
+
"middle": middle
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def create_benchmark_dataset(config: Config) -> int:
|
|
52
|
+
tokenizer = AutoTokenizer.from_pretrained(config.model_name)
|
|
53
|
+
|
|
54
|
+
dataset_features = Features({
|
|
55
|
+
"input_ids": Sequence(Value("int64")),
|
|
56
|
+
"attention_mask": Sequence(Value("int64")),
|
|
57
|
+
"labels": Sequence(Value("int64")),
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test_dataset = load_dataset(
|
|
61
|
+
"json",
|
|
62
|
+
data_files=str(config.test_dataset_path),
|
|
63
|
+
features=dataset_features,
|
|
64
|
+
streaming=True
|
|
65
|
+
)["train"]
|
|
66
|
+
|
|
67
|
+
shuffled_dataset = test_dataset.shuffle(
|
|
68
|
+
buffer_size=config.benchmark_shuffle_buffer_size,
|
|
69
|
+
seed=config.benchmark_shuffle_seed
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
benchmark_examples = []
|
|
73
|
+
added_examples_count = 0
|
|
74
|
+
for idx, example in enumerate(shuffled_dataset):
|
|
75
|
+
if added_examples_count >= config.benchmark_sample_size:
|
|
76
|
+
break
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
fim_parts = _extract_fim_parts(config, tokenizer, example["input_ids"])
|
|
80
|
+
except ValueError:
|
|
81
|
+
logger.warning(f"Could not extract fim parts from example {idx}, skipping")
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
if (len(fim_parts["middle_token_ids"]) < config.benchmark_min_fim_middle_tokens
|
|
85
|
+
or len(fim_parts["prefix_token_ids"]) == 0): # skip if no prefix
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
benchmark_examples.append(fim_parts)
|
|
89
|
+
added_examples_count += 1
|
|
90
|
+
|
|
91
|
+
with open(config.benchmark_dataset_path, 'w', encoding='utf-8') as f:
|
|
92
|
+
for example in benchmark_examples:
|
|
93
|
+
f.write(json.dumps(example) + '\n')
|
|
94
|
+
|
|
95
|
+
return len(benchmark_examples)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This shim module patches CodeBLEU so it uses tree-sitter-language-pack instead
|
|
3
|
+
of importing tree_sitter_<lang> modules directly. This allows CodeBLEU to
|
|
4
|
+
work with the project's existing tree-sitter-language-pack setup.
|
|
5
|
+
(A shim replaces specific internal functions of the originally imported
|
|
6
|
+
package with custom functions.)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Dict, List, Union
|
|
10
|
+
import tree_sitter_language_pack
|
|
11
|
+
import codebleu.utils
|
|
12
|
+
import codebleu.codebleu
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def shim_get_tree_sitter_language(lang: str):
|
|
16
|
+
try:
|
|
17
|
+
return tree_sitter_language_pack.get_language(lang)
|
|
18
|
+
except Exception as e:
|
|
19
|
+
raise ImportError(
|
|
20
|
+
f"Could not load language {lang} from tree_sitter_language_pack: {e}"
|
|
21
|
+
) from e
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# apply shim so all internal CodeBLEU calls use the tree_sitter_language_pack
|
|
25
|
+
codebleu.utils.get_tree_sitter_language = shim_get_tree_sitter_language
|
|
26
|
+
codebleu.codebleu.get_tree_sitter_language = shim_get_tree_sitter_language
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def codebleu_score(
|
|
30
|
+
references: Union[List[str], List[List[str]]],
|
|
31
|
+
predictions: List[str],
|
|
32
|
+
lang: str = "python",
|
|
33
|
+
weights: tuple[float, float, float, float] = (0.25, 0.25, 0.25, 0.25),
|
|
34
|
+
) -> Dict[str, float]:
|
|
35
|
+
from codebleu import calc_codebleu
|
|
36
|
+
return calc_codebleu(references, predictions, lang=lang, weights=weights)
|
|
37
|
+
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import math
|
|
3
|
+
from dataclasses import dataclass, field, fields
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import nltk
|
|
8
|
+
import torch
|
|
9
|
+
from omegaconf import OmegaConf, MISSING
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Config:
|
|
17
|
+
# --- Model (Mandatory Parameters) ---
|
|
18
|
+
model_name: str = MISSING
|
|
19
|
+
fim_prefix_token: str = MISSING
|
|
20
|
+
fim_suffix_token: str = MISSING
|
|
21
|
+
fim_middle_token: str = MISSING
|
|
22
|
+
fim_pad_token: str = MISSING
|
|
23
|
+
eos_token: str = MISSING
|
|
24
|
+
label_pad_token_id: int = MISSING
|
|
25
|
+
|
|
26
|
+
# --- Benchmark ---
|
|
27
|
+
benchmark_sample_size: int = 4
|
|
28
|
+
benchmark_min_fim_middle_tokens: int = 0
|
|
29
|
+
benchmark_shuffle_buffer_size: int = 10000000
|
|
30
|
+
benchmark_shuffle_seed: int = 42
|
|
31
|
+
|
|
32
|
+
# --- Generation ---
|
|
33
|
+
generation_max_new_tokens: int = 128
|
|
34
|
+
generation_do_sample: bool = False # note: if set to False temperature and top_p have no effect
|
|
35
|
+
generation_temperature: float = 0.7
|
|
36
|
+
generation_top_p: float = 0.95
|
|
37
|
+
|
|
38
|
+
# --- Metrics: CodeBLEU ---
|
|
39
|
+
codebleu_language: str = "c"
|
|
40
|
+
codebleu_metric_name: str = "codebleu"
|
|
41
|
+
# CodeBLEU weights (must sum to 1.0)
|
|
42
|
+
# see: https://arxiv.org/pdf/2009.10297 Section 4.4 for parameter suggestions 0.1, 0.1, 0.4, 0.4
|
|
43
|
+
codebleu_ngram_weight: float = 0.25 # token-level overlap (standard BLEU)
|
|
44
|
+
codebleu_weighted_ngram_weight: float = 0.25 # keyword-level overlap (importance-weighted)
|
|
45
|
+
codebleu_syntax_ast_weight: float = 0.25 # structural correctness (abstract syntax tree)
|
|
46
|
+
codebleu_dataflow_weight: float = 0.25 # logic consistency (variable dependency graph)
|
|
47
|
+
|
|
48
|
+
# --- Metrics: SentenceBLEU ---
|
|
49
|
+
sentencebleu_metric_name: str = "sentencebleu"
|
|
50
|
+
# sentence-BLEU weights (must sum to 1.0)
|
|
51
|
+
sentencebleu_ngram_weight_1: float = 0.25 # 1-gram
|
|
52
|
+
sentencebleu_ngram_weight_2: float = 0.25 # 2-gram
|
|
53
|
+
sentencebleu_ngram_weight_3: float = 0.25 # 3-gram
|
|
54
|
+
sentencebleu_ngram_weight_4: float = 0.25 # 4-gram
|
|
55
|
+
|
|
56
|
+
# --- Metrics: Other ---
|
|
57
|
+
exact_match_metric_name: str = "exact_match"
|
|
58
|
+
line_match_metric_name: str = "line_match"
|
|
59
|
+
line_match_number_of_lines: int = 2
|
|
60
|
+
perplexity_name: str = "perplexity"
|
|
61
|
+
|
|
62
|
+
# --- Execution Controls ---
|
|
63
|
+
trainer_checkpoint: str = "last" # "last"->use last checkpoint in trainer_checkpoints_dir_path
|
|
64
|
+
plot_only: bool = False # "skips" generate and evaluate, analyze only
|
|
65
|
+
benchmark_use_existing_dataset: bool = False
|
|
66
|
+
|
|
67
|
+
# --- Hardware Configuration ---
|
|
68
|
+
device: str = field(init=False)
|
|
69
|
+
model_dtype: Any = field(init=False) # # type hint Any because omegaconf does not support torch.dtype
|
|
70
|
+
_nltk_initialized: bool = field(init=False, default=False)
|
|
71
|
+
|
|
72
|
+
# --- Paths ---
|
|
73
|
+
workspace_path: Path | None = None
|
|
74
|
+
trainer_checkpoints_dir_path: Path = field(init=False)
|
|
75
|
+
test_dataset_path: Path = field(init=False)
|
|
76
|
+
evaluate_outputs_dir_path: Path = field(init=False)
|
|
77
|
+
benchmark_dataset_path: Path = field(init=False)
|
|
78
|
+
benchmark_evaluation_results_dir: Path = field(init=False)
|
|
79
|
+
benchmark_evaluation_results_path: Path = field(init=False)
|
|
80
|
+
benchmark_analysis_results_path: Path = field(init=False)
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def load_from_yaml(cls, yaml_path: Path) -> "Config":
|
|
84
|
+
if not yaml_path.exists():
|
|
85
|
+
raise FileNotFoundError(f"Configuration file not found at: {yaml_path}")
|
|
86
|
+
|
|
87
|
+
logger.info(f"Loading configuration from {yaml_path}")
|
|
88
|
+
config_dict = OmegaConf.structured(cls)
|
|
89
|
+
try:
|
|
90
|
+
yaml_file_node = OmegaConf.load(yaml_path)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
raise ValueError(f"Failed to load YAML config {yaml_path}") from e
|
|
93
|
+
|
|
94
|
+
yaml_file_dict = OmegaConf.to_container(yaml_file_node, resolve=True)
|
|
95
|
+
yaml_evaluate_dict = yaml_file_dict.get("evaluate", {})
|
|
96
|
+
|
|
97
|
+
yaml_evaluate_valid_dict = {}
|
|
98
|
+
# Filter YAML fields to include only those defined in the Config dataclass.
|
|
99
|
+
# This prevents OmegaConf from raising an AttributeError when encountering
|
|
100
|
+
# global YAML anchors or keys not present in the current Config dataclass.
|
|
101
|
+
for field in fields(cls):
|
|
102
|
+
if field.name in yaml_evaluate_dict:
|
|
103
|
+
yaml_evaluate_valid_dict[field.name] = yaml_evaluate_dict[field.name]
|
|
104
|
+
logger.debug(f"Filtered YAML configuration: {yaml_evaluate_valid_dict}")
|
|
105
|
+
|
|
106
|
+
merged_config_dict = OmegaConf.merge(config_dict, yaml_evaluate_valid_dict)
|
|
107
|
+
return OmegaConf.to_object(merged_config_dict)
|
|
108
|
+
|
|
109
|
+
def __post_init__(self) -> None:
|
|
110
|
+
self._setup_device_and_precision()
|
|
111
|
+
self._setup_paths()
|
|
112
|
+
self._ensure_output_paths_exist()
|
|
113
|
+
self._validate_metric_weights()
|
|
114
|
+
logger.debug("Config initialization complete.")
|
|
115
|
+
|
|
116
|
+
def _setup_device_and_precision(self) -> None:
|
|
117
|
+
if torch.cuda.is_available():
|
|
118
|
+
self.device = "cuda"
|
|
119
|
+
self.model_dtype = torch.bfloat16
|
|
120
|
+
elif torch.backends.mps.is_available():
|
|
121
|
+
self.device = "mps"
|
|
122
|
+
self.model_dtype = torch.float16
|
|
123
|
+
else:
|
|
124
|
+
self.device = "cpu"
|
|
125
|
+
self.model_dtype = torch.float32
|
|
126
|
+
logger.info(f"Execution environment: device={self.device}, dtype={self.model_dtype}")
|
|
127
|
+
|
|
128
|
+
def _validate_metric_weights(self) -> None:
|
|
129
|
+
codebleu_total_weight = (self.codebleu_ngram_weight + self.codebleu_weighted_ngram_weight +
|
|
130
|
+
self.codebleu_syntax_ast_weight + self.codebleu_dataflow_weight)
|
|
131
|
+
if not math.isclose(codebleu_total_weight, 1.0, rel_tol=1e-6):
|
|
132
|
+
raise ValueError(f"CodeBLEU weights must sum to 1.0, got {codebleu_total_weight}")
|
|
133
|
+
sentencebleu_total_weight = (self.sentencebleu_ngram_weight_1 + self.sentencebleu_ngram_weight_2 +
|
|
134
|
+
self.sentencebleu_ngram_weight_3 + self.sentencebleu_ngram_weight_4)
|
|
135
|
+
if not math.isclose(sentencebleu_total_weight, 1.0, rel_tol=1e-6):
|
|
136
|
+
raise ValueError(f"SentenceBLEU weights must sum to 1.0, got {sentencebleu_total_weight}")
|
|
137
|
+
|
|
138
|
+
def _setup_paths(self) -> None:
|
|
139
|
+
if self.workspace_path is None:
|
|
140
|
+
self.workspace_path = Path.cwd()
|
|
141
|
+
self.trainer_checkpoints_dir_path = self.workspace_path / "outputs" / "finetune" / "checkpoints"
|
|
142
|
+
self.test_dataset_path = self.workspace_path / "outputs" / "preprocess" / "results" / "datasets" / "test_dataset.jsonl"
|
|
143
|
+
self.evaluate_outputs_dir_path = self.workspace_path / "outputs" / "evaluate"
|
|
144
|
+
self.benchmark_dataset_path = self.evaluate_outputs_dir_path / "datasets" / "benchmark_dataset.jsonl"
|
|
145
|
+
self.benchmark_evaluation_results_dir = self.evaluate_outputs_dir_path / "results"
|
|
146
|
+
self.benchmark_evaluation_results_path = self.benchmark_evaluation_results_dir / "evaluation_results.jsonl"
|
|
147
|
+
self.benchmark_analysis_results_path = self.benchmark_evaluation_results_dir / "analysis_results.json"
|
|
148
|
+
logger.debug(f"Resolved workspace path to: {self.workspace_path}")
|
|
149
|
+
|
|
150
|
+
def _ensure_output_paths_exist(self) -> None:
|
|
151
|
+
paths = [
|
|
152
|
+
self.evaluate_outputs_dir_path,
|
|
153
|
+
self.benchmark_dataset_path,
|
|
154
|
+
self.benchmark_evaluation_results_dir,
|
|
155
|
+
self.benchmark_evaluation_results_path,
|
|
156
|
+
self.benchmark_analysis_results_path,
|
|
157
|
+
]
|
|
158
|
+
for path in paths:
|
|
159
|
+
if not path.parent.exists():
|
|
160
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
161
|
+
logger.debug(f"Created parent directory: {path.parent}")
|
|
162
|
+
else:
|
|
163
|
+
logger.debug(f"Parent directory already exists: {path.parent}")
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def metric_configs(self) -> list[tuple[str, bool]]:
|
|
167
|
+
return [
|
|
168
|
+
(self.sentencebleu_metric_name, True),
|
|
169
|
+
(self.codebleu_metric_name, True),
|
|
170
|
+
(self.exact_match_metric_name, True),
|
|
171
|
+
(self.line_match_metric_name, True),
|
|
172
|
+
(self.perplexity_name, False),
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
def ensure_nltk_initialized(self) -> None:
|
|
176
|
+
if self._nltk_initialized:
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
logger.info("Initializing NLTK data (punkt, punkt_tab)...")
|
|
180
|
+
try:
|
|
181
|
+
nltk.download('punkt', quiet=True)
|
|
182
|
+
nltk.download('punkt_tab', quiet=True)
|
|
183
|
+
self._nltk_initialized = True
|
|
184
|
+
logger.info("NLTK data initialized successfully.")
|
|
185
|
+
except Exception:
|
|
186
|
+
raise RuntimeError(f"NLTK initializerion failed." )
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from .config import Config
|
|
5
|
+
from .metrics import get_codebleu, get_exact_match, get_line_match, get_sentencebleu
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def evaluate_and_save(config: Config) -> None:
|
|
12
|
+
results_path = config.benchmark_evaluation_results_path
|
|
13
|
+
temp_path = results_path.with_name(f"{results_path.name}.tmp")
|
|
14
|
+
|
|
15
|
+
line_counter = 0
|
|
16
|
+
try:
|
|
17
|
+
with results_path.open("r") as evaluation_results_file, \
|
|
18
|
+
temp_path.open("w") as tmp_file:
|
|
19
|
+
|
|
20
|
+
for line in evaluation_results_file:
|
|
21
|
+
result = json.loads(line)
|
|
22
|
+
|
|
23
|
+
ref = result["reference_middle"]
|
|
24
|
+
base_gen = result["base_generated_middle"]
|
|
25
|
+
lora_gen = result["lora_generated_middle"]
|
|
26
|
+
|
|
27
|
+
base_cb, cb_valid = get_codebleu(config, ref, base_gen)
|
|
28
|
+
lora_cb, _ = get_codebleu(config, ref, lora_gen)
|
|
29
|
+
|
|
30
|
+
base_sb = get_sentencebleu(config, ref, base_gen)
|
|
31
|
+
lora_sb = get_sentencebleu(config, ref, lora_gen)
|
|
32
|
+
|
|
33
|
+
base_em = get_exact_match(ref, base_gen)
|
|
34
|
+
lora_em = get_exact_match(ref, lora_gen)
|
|
35
|
+
|
|
36
|
+
base_lm = get_line_match(config, ref, base_gen)
|
|
37
|
+
lora_lm = get_line_match(config, ref, lora_gen)
|
|
38
|
+
|
|
39
|
+
result.update({
|
|
40
|
+
"base_codebleu": base_cb,
|
|
41
|
+
"lora_codebleu": lora_cb,
|
|
42
|
+
"codebleu_valid": cb_valid,
|
|
43
|
+
"base_sentencebleu": base_sb,
|
|
44
|
+
"lora_sentencebleu": lora_sb,
|
|
45
|
+
"base_exact_match": base_em,
|
|
46
|
+
"lora_exact_match": lora_em,
|
|
47
|
+
"base_line_match": base_lm,
|
|
48
|
+
"lora_line_match": lora_lm,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
tmp_file.write(json.dumps(result) + "\n")
|
|
52
|
+
line_counter += 1
|
|
53
|
+
|
|
54
|
+
if line_counter % 10 == 0:
|
|
55
|
+
logger.info(f"Processed {line_counter} examples...")
|
|
56
|
+
|
|
57
|
+
temp_path.replace(results_path)
|
|
58
|
+
logger.info(f"Successfully evaluated and saved {line_counter} examples to {config.benchmark_evaluation_results_path}.")
|
|
59
|
+
|
|
60
|
+
except Exception as e:
|
|
61
|
+
if temp_path.exists():
|
|
62
|
+
temp_path.unlink()
|
|
63
|
+
raise RuntimeError(f"Evaluation failed at example {line_counter}: {e}") from e
|