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.
@@ -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