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,145 @@
|
|
|
1
|
+
import gc
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import math
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import torch
|
|
8
|
+
from peft import PeftModel
|
|
9
|
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
10
|
+
|
|
11
|
+
from .config import Config
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_lora_model(config: Config, checkpoint_path: Path) ->AutoModelForCausalLM:
|
|
18
|
+
# load base model to CPU first to prevent VRAM fragmentation/OOM
|
|
19
|
+
base_model = AutoModelForCausalLM.from_pretrained(
|
|
20
|
+
pretrained_model_name_or_path=config.model_name,
|
|
21
|
+
dtype=config.model_dtype,
|
|
22
|
+
low_cpu_mem_usage=True
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# load and attach LoRA adapter and move to device (CUDA, MPS or remains on CPU)
|
|
26
|
+
lora_model = PeftModel.from_pretrained(
|
|
27
|
+
model=base_model,
|
|
28
|
+
model_id=checkpoint_path
|
|
29
|
+
).to(config.device)
|
|
30
|
+
|
|
31
|
+
lora_model.eval()
|
|
32
|
+
|
|
33
|
+
logger.info(f"Loaded LoRA model from checkpoint {checkpoint_path} to device {config.device}")
|
|
34
|
+
return lora_model
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _load_tokenizer(config: Config) -> AutoTokenizer:
|
|
38
|
+
tokenizer = AutoTokenizer.from_pretrained(config.model_name)
|
|
39
|
+
tokenizer.pad_token = config.fim_pad_token
|
|
40
|
+
tokenizer.padding_side = "right"
|
|
41
|
+
return tokenizer
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _get_fim_perplexity(config: Config, model: AutoModelForCausalLM,
|
|
45
|
+
perplexity_input_token_ids: list[int], perplexity_label_token_ids: list[int]) -> float:
|
|
46
|
+
"""
|
|
47
|
+
FIM perplexity: Measures model confidence in the ground truth reference middle code.
|
|
48
|
+
(How surprised is the model by the ground truth reference middle code).
|
|
49
|
+
Lower perplexity indicates higher confidence. perplexity = exp(loss).
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
input_tensor = torch.tensor([perplexity_input_token_ids], device=config.device)
|
|
53
|
+
label_tensor = torch.tensor([perplexity_label_token_ids], device=config.device)
|
|
54
|
+
with torch.inference_mode():
|
|
55
|
+
outputs = model(input_ids=input_tensor,
|
|
56
|
+
labels=label_tensor)
|
|
57
|
+
loss = outputs.loss
|
|
58
|
+
|
|
59
|
+
return math.exp(loss.item())
|
|
60
|
+
|
|
61
|
+
except Exception as e:
|
|
62
|
+
logger.warning(f"Perplexity calcualtin failed, returning inf: {e}")
|
|
63
|
+
return float('inf')
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _generate(config: Config, model: AutoModelForCausalLM, tokenizer: AutoTokenizer, prompt_token_ids: list[int]) -> list:
|
|
67
|
+
model.eval()
|
|
68
|
+
prompt_token_ids_tensor = torch.tensor([prompt_token_ids], device=config.device)
|
|
69
|
+
with torch.inference_mode():
|
|
70
|
+
generated_token_ids_tensor = model.generate(
|
|
71
|
+
input_ids=prompt_token_ids_tensor,
|
|
72
|
+
max_new_tokens=config.generation_max_new_tokens,
|
|
73
|
+
do_sample=config.generation_do_sample,
|
|
74
|
+
temperature=config.generation_temperature,
|
|
75
|
+
top_p=config.generation_top_p,
|
|
76
|
+
pad_token_id=tokenizer.pad_token_id
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# slice the output, take everything after the input_length, model.generate() functin returns the whole example, not only the generated text
|
|
80
|
+
generated_middle_token_ids_tensor = generated_token_ids_tensor[0][prompt_token_ids_tensor.shape[1] :]
|
|
81
|
+
generated_middle_token_ids = generated_middle_token_ids_tensor.tolist()
|
|
82
|
+
|
|
83
|
+
return generated_middle_token_ids
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _clear_hardware_cache(config: Config) -> None:
|
|
87
|
+
gc.collect()
|
|
88
|
+
if config.device == "cuda":
|
|
89
|
+
torch.cuda.synchronize()
|
|
90
|
+
torch.cuda.empty_cache()
|
|
91
|
+
elif config.device == "mps":
|
|
92
|
+
torch.mps.empty_cache()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def generate_and_save(config: Config, checkpoint_path: Path):
|
|
96
|
+
lora_model = _load_lora_model(config, checkpoint_path)
|
|
97
|
+
tokenizer = _load_tokenizer(config)
|
|
98
|
+
fim_prefix_token_id = tokenizer.convert_tokens_to_ids(config.fim_prefix_token)
|
|
99
|
+
fim_suffix_token_id = tokenizer.convert_tokens_to_ids(config.fim_suffix_token)
|
|
100
|
+
fim_middle_token_id = tokenizer.convert_tokens_to_ids(config.fim_middle_token)
|
|
101
|
+
|
|
102
|
+
line_counter = 0
|
|
103
|
+
try:
|
|
104
|
+
with config.benchmark_dataset_path.open("r") as benchmark_dataset_file, \
|
|
105
|
+
config.benchmark_evaluation_results_path.open("w") as evaluation_results_file:
|
|
106
|
+
|
|
107
|
+
for line in benchmark_dataset_file:
|
|
108
|
+
benchmark_example = json.loads(line)
|
|
109
|
+
prompt_token_ids = ([fim_prefix_token_id] + benchmark_example["prefix_token_ids"] +
|
|
110
|
+
[fim_suffix_token_id] + benchmark_example["suffix_token_ids"] +
|
|
111
|
+
[fim_middle_token_id])
|
|
112
|
+
|
|
113
|
+
perplexity_input_token_ids = benchmark_example["example_token_ids"]
|
|
114
|
+
perplexity_label_token_ids = perplexity_input_token_ids.copy()
|
|
115
|
+
perplexity_label_token_ids[:len(prompt_token_ids)] = [config.label_pad_token_id] * len(prompt_token_ids) # mask labels all except ground truth reference middle tokens
|
|
116
|
+
|
|
117
|
+
lora_generated_middle_token_ids = _generate(config, lora_model, tokenizer, prompt_token_ids)
|
|
118
|
+
lora_perplexity = _get_fim_perplexity(config, lora_model, perplexity_input_token_ids, perplexity_label_token_ids)
|
|
119
|
+
|
|
120
|
+
with lora_model.disable_adapter():
|
|
121
|
+
base_generated_middle_token_ids = _generate(config, lora_model, tokenizer, prompt_token_ids)
|
|
122
|
+
base_perplexity = _get_fim_perplexity(config, lora_model, perplexity_input_token_ids, perplexity_label_token_ids)
|
|
123
|
+
|
|
124
|
+
reference_middle = benchmark_example["middle"]
|
|
125
|
+
lora_generated_middle = tokenizer.decode(lora_generated_middle_token_ids, skip_special_tokens=True)
|
|
126
|
+
base_generated_middle = tokenizer.decode(base_generated_middle_token_ids, skip_special_tokens=True)
|
|
127
|
+
result = {
|
|
128
|
+
"example_id": line_counter,
|
|
129
|
+
"reference_middle": reference_middle,
|
|
130
|
+
"base_generated_middle": base_generated_middle,
|
|
131
|
+
"lora_generated_middle": lora_generated_middle,
|
|
132
|
+
"base_perplexity": base_perplexity,
|
|
133
|
+
"lora_perplexity": lora_perplexity
|
|
134
|
+
}
|
|
135
|
+
evaluation_results_file.write(json.dumps(result) + "\n")
|
|
136
|
+
|
|
137
|
+
line_counter += 1
|
|
138
|
+
if line_counter % 10 == 0:
|
|
139
|
+
_clear_hardware_cache(config)
|
|
140
|
+
logger.info(f"Processed {line_counter} benchmark examples")
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
raise RuntimeError(f"Generation failed at example {line_counter}: {e}") from e
|
|
144
|
+
|
|
145
|
+
logger.info(f"Successfully generated and saved {line_counter} number of examples to {config.benchmark_evaluation_results_path}.")
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
from nltk.tokenize import word_tokenize
|
|
4
|
+
from nltk.translate.bleu_score import SmoothingFunction, sentence_bleu
|
|
5
|
+
|
|
6
|
+
from .codebleu_shim import codebleu_score
|
|
7
|
+
from .config import Config
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _codebleu_structure_valid(config: Config, reference: str) -> bool:
|
|
14
|
+
"""
|
|
15
|
+
Checks if the reference code is structurally complex enough for CodeBLEU.
|
|
16
|
+
Performs a self-match test focusing only on Syntax (AST) and Data-flow.
|
|
17
|
+
If the reference is too simple (e.g., import blocks, comment)
|
|
18
|
+
to build these structures, the example is skipped.
|
|
19
|
+
"""
|
|
20
|
+
# suppress root logger warnings that are raised if it is not possible to calculate all 4 metrics
|
|
21
|
+
root_logger = logging.getLogger()
|
|
22
|
+
original_level = root_logger.getEffectiveLevel()
|
|
23
|
+
root_logger.setLevel(logging.ERROR)
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
test_weights = (0.0, 0.0, 0.5, 0.5)
|
|
27
|
+
result = codebleu_score([reference], [reference],
|
|
28
|
+
lang=config.codebleu_language,
|
|
29
|
+
weights=test_weights)
|
|
30
|
+
|
|
31
|
+
syntax_valid = result.get('syntax_match_score', 0) > 0
|
|
32
|
+
dataflow_valid = result.get('dataflow_match_score', 0) > 0
|
|
33
|
+
return syntax_valid and dataflow_valid
|
|
34
|
+
except Exception:
|
|
35
|
+
return False
|
|
36
|
+
finally:
|
|
37
|
+
# restore logger
|
|
38
|
+
root_logger.setLevel(original_level)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_codebleu(config: Config, reference: str, prediction: str) -> tuple[float, bool]:
|
|
42
|
+
"""
|
|
43
|
+
CodeBLEU: Computes weighted combination of four different similarity metrics:
|
|
44
|
+
1. N-gram Match: Standard surface-level text overlap.
|
|
45
|
+
2. Weighted N-gram: Text overlap with priority on keywords (if, else, etc.).
|
|
46
|
+
3. Syntax (AST): Structural similarity between Abstract Syntax Trees.
|
|
47
|
+
4. Data-flow: Logical similarity based on variable dependencies and usage.
|
|
48
|
+
|
|
49
|
+
Examples are skipped if structural components (AST/Data-flow) cannot be
|
|
50
|
+
extracted from the reference. This prevents the final average score from
|
|
51
|
+
being unfairly lowered by snippets that cannot be properly parsed,
|
|
52
|
+
ensuring a more accurate representation of model quality.
|
|
53
|
+
|
|
54
|
+
Standard weights are: [0.25, 0.25, 0.25, 0.25].
|
|
55
|
+
CodeBLEU = (codebleu_ngram_weight * ngram_score) +
|
|
56
|
+
(codebleu_weighted_ngram_weight * weighted_ngram_score) +
|
|
57
|
+
(codebleu_syntax_ast_weight * syntax_ast_score) +
|
|
58
|
+
(codebleu_dataflow_weight * dataflow_score)
|
|
59
|
+
"""
|
|
60
|
+
if not _codebleu_structure_valid(config, reference):
|
|
61
|
+
return (0.0, False)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
codebleu_algorithm_weights = (
|
|
65
|
+
config.codebleu_ngram_weight,
|
|
66
|
+
config.codebleu_weighted_ngram_weight,
|
|
67
|
+
config.codebleu_syntax_ast_weight,
|
|
68
|
+
config.codebleu_dataflow_weight
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
result = codebleu_score(
|
|
72
|
+
[reference], [prediction],
|
|
73
|
+
lang=config.codebleu_language,
|
|
74
|
+
weights=codebleu_algorithm_weights
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return (float(result['codebleu']), True)
|
|
78
|
+
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.warning(f"CodeBLEU calculation failed, returning (0.0, False): {e}")
|
|
81
|
+
return (0.0, False)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_sentencebleu(config: Config, reference: str, prediction: str) -> float:
|
|
85
|
+
"""
|
|
86
|
+
SentenceBLEU: Measures n-gram overlap between reference and prediction.
|
|
87
|
+
It rewards matching sequences of words (1-4) and uses smoothing
|
|
88
|
+
(Method1: Adds a tiny epsilon to all n-gram counts) to prevent a total
|
|
89
|
+
0.0 score when long sequences (e.g. 4-grams) don't match exactly.
|
|
90
|
+
"""
|
|
91
|
+
config.ensure_nltk_initialized() # make sure that required nltk downloads are done
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
reference_tokens = word_tokenize(reference)
|
|
95
|
+
prediction_tokens = word_tokenize(prediction)
|
|
96
|
+
|
|
97
|
+
weights = (
|
|
98
|
+
config.sentencebleu_ngram_weight_1,
|
|
99
|
+
config.sentencebleu_ngram_weight_2,
|
|
100
|
+
config.sentencebleu_ngram_weight_3,
|
|
101
|
+
config.sentencebleu_ngram_weight_4
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
smoothing = SmoothingFunction().method1
|
|
105
|
+
|
|
106
|
+
score = sentence_bleu(
|
|
107
|
+
[reference_tokens],
|
|
108
|
+
prediction_tokens,
|
|
109
|
+
weights=weights,
|
|
110
|
+
smoothing_function=smoothing
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return float(score)
|
|
114
|
+
|
|
115
|
+
except Exception as e:
|
|
116
|
+
logger.warning(f"SentenceBLEU calculation failed, returning 0.0: {e}")
|
|
117
|
+
return 0.0
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def get_exact_match(reference: str, prediction: str) -> float:
|
|
121
|
+
"""Exact match is 1.0 if identical, 0.0 otherwise. Collapese all whitespaces."""
|
|
122
|
+
try:
|
|
123
|
+
# re.sub(r'\s+', ' ', text.strip()): Collapses whitespace to compare logic regardless of formatting.
|
|
124
|
+
ref_norm = re.sub(r'\s+', ' ', reference.strip())
|
|
125
|
+
pred_norm = re.sub(r'\s+', ' ', prediction.strip())
|
|
126
|
+
|
|
127
|
+
if ref_norm == pred_norm:
|
|
128
|
+
return 1.0
|
|
129
|
+
else:
|
|
130
|
+
return 0.0
|
|
131
|
+
except Exception as e:
|
|
132
|
+
logger.warning(f"Exact match calculation failed, returning 0.0: {e}")
|
|
133
|
+
return 0.0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def get_line_match(config: Config, reference: str, prediction: str) -> float:
|
|
137
|
+
"""Check if the first n lines match, ignoring trailing whitespace."""
|
|
138
|
+
try:
|
|
139
|
+
n = config.line_match_number_of_lines
|
|
140
|
+
|
|
141
|
+
# line.rstrip(): Removes trailing whitespace while preserving leading indentation.
|
|
142
|
+
ref_lines_stripped = []
|
|
143
|
+
for line in reference.splitlines()[:n]:
|
|
144
|
+
ref_lines_stripped.append(line.rstrip())
|
|
145
|
+
|
|
146
|
+
pred_lines_stripped = []
|
|
147
|
+
for line in prediction.splitlines()[:n]:
|
|
148
|
+
pred_lines_stripped.append(line.rstrip())
|
|
149
|
+
|
|
150
|
+
# Ensure both lists have the required number of lines
|
|
151
|
+
if len(pred_lines_stripped) < n or len(ref_lines_stripped) < n:
|
|
152
|
+
return 0.0
|
|
153
|
+
|
|
154
|
+
if pred_lines_stripped == ref_lines_stripped:
|
|
155
|
+
return 1.0
|
|
156
|
+
else:
|
|
157
|
+
return 0.0
|
|
158
|
+
except Exception as e:
|
|
159
|
+
logger.warning(f"Line match calculation failed, returning 0.0: {e}")
|
|
160
|
+
return 0.0
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
import logging.config
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from transformers.trainer_utils import get_last_checkpoint
|
|
8
|
+
|
|
9
|
+
from .config import Config
|
|
10
|
+
from .benchmark import create_benchmark_dataset
|
|
11
|
+
from .generate import generate_and_save
|
|
12
|
+
from .evaluate import evaluate_and_save
|
|
13
|
+
from .analyze import analyze_metric, save_all_metric_stats, get_plot_path, plot_metric_and_save, plot_all_metric_averages_and_save
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _parse_args() -> argparse.Namespace:
|
|
20
|
+
parser = argparse.ArgumentParser(description="Run evaluation")
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--config",
|
|
23
|
+
type=Path,
|
|
24
|
+
required=True,
|
|
25
|
+
help="Path to the pipeline YAML configuration file.",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--log-level",
|
|
29
|
+
type=str,
|
|
30
|
+
default="INFO",
|
|
31
|
+
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
|
32
|
+
help="Logging level.",
|
|
33
|
+
)
|
|
34
|
+
return parser.parse_args()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _setup_logger(log_level: str) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Configure the root logger ("") to capture logs from the entry point (__main__),
|
|
40
|
+
all internal sub-modules, and third-party libraries via a single handler.
|
|
41
|
+
"""
|
|
42
|
+
logger_config = {
|
|
43
|
+
"version": 1,
|
|
44
|
+
"disable_existing_loggers": False,
|
|
45
|
+
"formatters": {
|
|
46
|
+
"standard": {
|
|
47
|
+
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"handlers": {
|
|
51
|
+
"stderr_handler": {
|
|
52
|
+
"level": log_level,
|
|
53
|
+
"class": "logging.StreamHandler",
|
|
54
|
+
"formatter": "standard",
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"loggers": {
|
|
58
|
+
"": { # "" corresponds to root logger
|
|
59
|
+
"handlers": ["stderr_handler"],
|
|
60
|
+
"level": log_level,
|
|
61
|
+
"propagate": False,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
logging.config.dictConfig(logger_config)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _silence_noisy_third_party_loggers() -> None:
|
|
69
|
+
"""Reduce log level of known noisy third‑party loggers."""
|
|
70
|
+
logging.getLogger("matplotlib").setLevel(logging.WARNING)
|
|
71
|
+
logging.getLogger("matplotlib.font_manager").setLevel(logging.WARNING)
|
|
72
|
+
logging.getLogger("nltk").setLevel(logging.WARNING)
|
|
73
|
+
# add more as needed
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _ensure_checkpoints(config: Config) -> None:
|
|
77
|
+
checkpoints_dir = config.trainer_checkpoints_dir_path
|
|
78
|
+
if not checkpoints_dir.exists():
|
|
79
|
+
raise RuntimeError("Checkpoint directory not found.")
|
|
80
|
+
if not list(checkpoints_dir.iterdir()):
|
|
81
|
+
raise RuntimeError("No checkpoints in directory.")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def run(config: Config) -> None:
|
|
85
|
+
if not config.benchmark_use_existing_dataset or not config.benchmark_dataset_path.exists():
|
|
86
|
+
dataset_len = create_benchmark_dataset(config)
|
|
87
|
+
logger.info(f"Created new benchmark dataset '{config.benchmark_dataset_path}' with '{dataset_len}' examples")
|
|
88
|
+
else:
|
|
89
|
+
logger.info(f"Proceeding with existing file '{config.benchmark_dataset_path}'...")
|
|
90
|
+
|
|
91
|
+
if not config.plot_only:
|
|
92
|
+
_ensure_checkpoints(config)
|
|
93
|
+
if config.trainer_checkpoint == "last":
|
|
94
|
+
checkpoint_path = get_last_checkpoint(config.trainer_checkpoints_dir_path)
|
|
95
|
+
else:
|
|
96
|
+
checkpoint_path = config.trainer_checkpoints_dir_path / config.trainer_checkpoint
|
|
97
|
+
generate_and_save(config, checkpoint_path)
|
|
98
|
+
evaluate_and_save(config)
|
|
99
|
+
|
|
100
|
+
all_metric_stats_np = []
|
|
101
|
+
for metric_name, higher_is_better in config.metric_configs:
|
|
102
|
+
metric_stats_np = analyze_metric(config, metric_name, higher_is_better)
|
|
103
|
+
plot_path = get_plot_path(config.benchmark_evaluation_results_dir, metric_name)
|
|
104
|
+
plot_metric_and_save(metric_stats_np, metric_name, plot_path)
|
|
105
|
+
all_metric_stats_np.append(metric_stats_np)
|
|
106
|
+
|
|
107
|
+
all_metric_averages_plot_path = get_plot_path(config.benchmark_evaluation_results_dir, "all_metric_averages")
|
|
108
|
+
plot_all_metric_averages_and_save(all_metric_stats_np, all_metric_averages_plot_path)
|
|
109
|
+
save_all_metric_stats(config, all_metric_stats_np)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def main():
|
|
113
|
+
user_args = _parse_args()
|
|
114
|
+
_setup_logger(user_args.log_level)
|
|
115
|
+
_silence_noisy_third_party_loggers()
|
|
116
|
+
try:
|
|
117
|
+
evaluate_config = Config.load_from_yaml(user_args.config)
|
|
118
|
+
run(evaluate_config)
|
|
119
|
+
except Exception:
|
|
120
|
+
logger.exception("Evaluation failed")
|
|
121
|
+
sys.exit(1)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
main()
|
|
File without changes
|
|
@@ -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 List, Any
|
|
6
|
+
|
|
7
|
+
import torch
|
|
8
|
+
from omegaconf import OmegaConf, MISSING
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Config:
|
|
16
|
+
# --- Mandatory Parameters ---
|
|
17
|
+
model_name: str = MISSING
|
|
18
|
+
fim_pad_token: str = MISSING
|
|
19
|
+
label_pad_token_id: int = MISSING
|
|
20
|
+
|
|
21
|
+
# --- Model Settings ---
|
|
22
|
+
model_attn_implementation: str = "sdpa" # sdpa = built-in PyTorch implementation of scaled dot product attention, imporves performance and memory efficiency
|
|
23
|
+
model_dtype: Any = field(init=False) # changed to Any to bypass OmegaConf validation
|
|
24
|
+
|
|
25
|
+
# --- LoRA Settings ---
|
|
26
|
+
lora_r: int = 32
|
|
27
|
+
lora_alpha: int = 64
|
|
28
|
+
lora_dropout: float = 0.1
|
|
29
|
+
lora_bias: str = "none"
|
|
30
|
+
lora_target_modules: List[str] = field(default_factory=lambda: [
|
|
31
|
+
"q_proj", "v_proj", "k_proj", "o_proj",
|
|
32
|
+
"gate_proj", "down_proj", "up_proj"
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
# --- Trainer Hyperparameters ---
|
|
36
|
+
trainer_resume_from_checkpoint: str | None = "last"
|
|
37
|
+
trainer_clear_checkpoint_dir: bool = False
|
|
38
|
+
trainer_num_train_epochs: int = 1
|
|
39
|
+
trainer_per_device_train_batch_size: int = 2
|
|
40
|
+
trainer_per_device_eval_batch_size: int = 2
|
|
41
|
+
trainer_gradient_accumulation_steps: int = 32 # simulates a larger effective batch size, number of forward/backward passes to accumulate before performing one optimizer step
|
|
42
|
+
trainer_learning_rate: float = 2e-5
|
|
43
|
+
trainer_weight_decay: float = 0.1
|
|
44
|
+
trainer_max_grad_norm: float = 1.0
|
|
45
|
+
trainer_lr_scheduler_type: str = "cosine"
|
|
46
|
+
trainer_warmup_steps: int = 50
|
|
47
|
+
trainer_gradient_checkpointing: bool = True # saves memory by storing only key "checkpoint" activations and re-calculating intermediate ones during the backward pass
|
|
48
|
+
trainer_max_steps: int = field(init=False)
|
|
49
|
+
|
|
50
|
+
# --- Logging and Evaluation Strategy ---
|
|
51
|
+
trainer_logging_steps: int = 10 # average training loss over trainer_logging_steps period is calculated and logged
|
|
52
|
+
trainer_eval_strategy: str = "steps"
|
|
53
|
+
trainer_eval_steps: int = 100
|
|
54
|
+
trainer_save_strategy: str = "steps"
|
|
55
|
+
trainer_save_steps: int = 100
|
|
56
|
+
trainer_logging_strategy: str = "steps"
|
|
57
|
+
|
|
58
|
+
# --- Dataset ---
|
|
59
|
+
dataset_shuffle_buffer_size: int = 50000
|
|
60
|
+
dataset_shuffle_seed: int = 0
|
|
61
|
+
dataset_train_dataset_length: int = field(init=False)
|
|
62
|
+
|
|
63
|
+
# --- Hardware Configuration ---
|
|
64
|
+
device: str = field(init=False)
|
|
65
|
+
|
|
66
|
+
# --- Path Management ---
|
|
67
|
+
workspace_path: Path | None = None
|
|
68
|
+
train_dataset_path: Path = field(init=False)
|
|
69
|
+
eval_dataset_path: Path = field(init=False)
|
|
70
|
+
finetune_outputs_dir_path: Path = field(init=False)
|
|
71
|
+
trainer_checkpoints_dir_path: Path = field(init=False)
|
|
72
|
+
trainer_log_path: Path = field(init=False)
|
|
73
|
+
trainer_plot_path: Path = field(init=False)
|
|
74
|
+
lora_adapter_path: Path = field(init=False)
|
|
75
|
+
lora_model_path: Path = field(init=False)
|
|
76
|
+
trainer_model_merge_offload_folder_path: Path = field(init=False)
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def load_from_yaml(cls, yaml_path: Path) -> "Config":
|
|
80
|
+
if not yaml_path.exists():
|
|
81
|
+
raise FileNotFoundError(f"Config file not found: {yaml_path}")
|
|
82
|
+
|
|
83
|
+
logger.info(f"Loading configuration from {yaml_path}")
|
|
84
|
+
config_dict = OmegaConf.structured(cls)
|
|
85
|
+
try:
|
|
86
|
+
yaml_file_node = OmegaConf.load(yaml_path)
|
|
87
|
+
except Exception as e:
|
|
88
|
+
raise ValueError(f"Failed to load YAML config {yaml_path}") from e
|
|
89
|
+
|
|
90
|
+
yaml_file_dict = OmegaConf.to_container(yaml_file_node, resolve=True)
|
|
91
|
+
yaml_finetune_dict = yaml_file_dict.get("finetune", {})
|
|
92
|
+
|
|
93
|
+
yaml_finetune_valid_dict = {}
|
|
94
|
+
# Filter YAML fields to include only those defined in the Config dataclass.
|
|
95
|
+
# This prevents OmegaConf from raising an AttributeError when encountering
|
|
96
|
+
# global YAML anchors or keys not present in the current Config dataclass.
|
|
97
|
+
for field in fields(cls):
|
|
98
|
+
if field.name in yaml_finetune_dict:
|
|
99
|
+
yaml_finetune_valid_dict[field.name] = yaml_finetune_dict[field.name]
|
|
100
|
+
logger.debug(f"Filtered YAML configuration: {yaml_finetune_valid_dict}")
|
|
101
|
+
|
|
102
|
+
merged_config_dict = OmegaConf.merge(config_dict, yaml_finetune_valid_dict)
|
|
103
|
+
return OmegaConf.to_object(merged_config_dict)
|
|
104
|
+
|
|
105
|
+
def __post_init__(self) -> None:
|
|
106
|
+
self._setup_device_and_precision()
|
|
107
|
+
self._setup_paths()
|
|
108
|
+
self._ensure_output_paths_exist()
|
|
109
|
+
self.dataset_train_dataset_length = self._get_dataset_length(self.train_dataset_path)
|
|
110
|
+
self.trainer_max_steps = self._calculate_max_steps()
|
|
111
|
+
|
|
112
|
+
def _setup_device_and_precision(self) -> None:
|
|
113
|
+
if torch.cuda.is_available():
|
|
114
|
+
self.device = "cuda"
|
|
115
|
+
self.model_dtype = torch.bfloat16
|
|
116
|
+
elif torch.backends.mps.is_available():
|
|
117
|
+
self.device = "mps"
|
|
118
|
+
self.model_dtype = torch.float16
|
|
119
|
+
else:
|
|
120
|
+
self.device = "cpu"
|
|
121
|
+
self.model_dtype = torch.float32
|
|
122
|
+
logger.info(f"Execution environment: device={self.device}, dtype={self.model_dtype}")
|
|
123
|
+
|
|
124
|
+
def _setup_paths(self) -> None:
|
|
125
|
+
if self.workspace_path is None:
|
|
126
|
+
self.workspace_path = Path.cwd()
|
|
127
|
+
self.train_dataset_path = self.workspace_path / "outputs" / "preprocess" / "results" / "datasets" / "train_dataset.jsonl"
|
|
128
|
+
self.eval_dataset_path = self.workspace_path / "outputs" / "preprocess" / "results" / "datasets" / "eval_dataset.jsonl"
|
|
129
|
+
self.finetune_outputs_dir_path = self.workspace_path / "outputs" / "finetune"
|
|
130
|
+
self.trainer_checkpoints_dir_path = self.finetune_outputs_dir_path / "checkpoints"
|
|
131
|
+
self.trainer_model_merge_offload_folder_path = self.finetune_outputs_dir_path / "trainer_model_merge_offload_folder"
|
|
132
|
+
self.trainer_log_path = self.finetune_outputs_dir_path / "results" / "trainer_log.json"
|
|
133
|
+
self.trainer_plot_path = self.finetune_outputs_dir_path / "results" / "trainer_loss_plot.png"
|
|
134
|
+
self.lora_adapter_path = self.finetune_outputs_dir_path / "results" / "lora_adapter"
|
|
135
|
+
self.lora_model_path = self.finetune_outputs_dir_path / "results" / "lora_model"
|
|
136
|
+
logger.debug(f"Resolved workspace path to: {self.workspace_path}")
|
|
137
|
+
|
|
138
|
+
def _ensure_output_paths_exist(self) -> None:
|
|
139
|
+
paths = [
|
|
140
|
+
self.finetune_outputs_dir_path,
|
|
141
|
+
self.trainer_checkpoints_dir_path,
|
|
142
|
+
self.trainer_model_merge_offload_folder_path,
|
|
143
|
+
self.trainer_log_path,
|
|
144
|
+
self.trainer_plot_path,
|
|
145
|
+
self.lora_adapter_path,
|
|
146
|
+
self.lora_model_path
|
|
147
|
+
]
|
|
148
|
+
for path in paths:
|
|
149
|
+
if not path.parent.exists():
|
|
150
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
151
|
+
logger.debug(f"Created parent directory: {path.parent}")
|
|
152
|
+
else:
|
|
153
|
+
logger.debug(f"Parent directory already exists: {path.parent}")
|
|
154
|
+
|
|
155
|
+
def _get_dataset_length(self, path: Path) -> int:
|
|
156
|
+
"""
|
|
157
|
+
Calculates line count for streaming dataset progress estimation.
|
|
158
|
+
We need to do this because we load it as a streaming dataset iterator, which can only be iterated once.
|
|
159
|
+
"""
|
|
160
|
+
if not path.exists():
|
|
161
|
+
raise FileNotFoundError( f"Training dataset not found at expected path: {path}. Ensure the dataset file exists before initializing the Config.")
|
|
162
|
+
count = 0
|
|
163
|
+
with open(path, "r", encoding="utf-8") as file:
|
|
164
|
+
for _ in file:
|
|
165
|
+
count += 1
|
|
166
|
+
logger.debug(f"Dataset length for '{path.name}': {count} lines")
|
|
167
|
+
return count
|
|
168
|
+
|
|
169
|
+
def _calculate_max_steps(self) -> int:
|
|
170
|
+
"""
|
|
171
|
+
Calculates total steps based on effective batch size and dataset length.
|
|
172
|
+
Because we use streaming dataset iterators for efficiency, we cannot use num_train_epochs trainer class parameter directly.
|
|
173
|
+
Instead, we need to calculate max_steps and pass it to the trainer.
|
|
174
|
+
"""
|
|
175
|
+
if self.dataset_train_dataset_length == 0:
|
|
176
|
+
logger.warning("Dataset length is 0; max_steps will be 0.")
|
|
177
|
+
return 0
|
|
178
|
+
|
|
179
|
+
effective_batch_size = (self.trainer_per_device_train_batch_size * self.trainer_gradient_accumulation_steps)
|
|
180
|
+
if effective_batch_size == 0:
|
|
181
|
+
raise ValueError("Effective batch size (batch_size * grad_accum) cannot be zero.")
|
|
182
|
+
steps_per_epoch = math.ceil(self.dataset_train_dataset_length / effective_batch_size)
|
|
183
|
+
max_steps = steps_per_epoch * self.trainer_num_train_epochs
|
|
184
|
+
logger.debug(f"Calculated training schedule: {max_steps} total steps ({steps_per_epoch} steps/epoch for {self.trainer_num_train_epochs} epochs)")
|
|
185
|
+
return max_steps
|
|
186
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
|
|
5
|
+
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
|
|
6
|
+
|
|
7
|
+
from .config import Config
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_and_configure_lora_model(config: Config) -> AutoModelForCausalLM:
|
|
14
|
+
if config.device == "cuda":
|
|
15
|
+
bnb_config = BitsAndBytesConfig(
|
|
16
|
+
load_in_4bit=True,
|
|
17
|
+
bnb_4bit_quant_type="nf4", # nf4 (NormalFloat4) = optimal for LLM training, because llm weights follwo normal distribution
|
|
18
|
+
bnb_4bit_compute_type=config.model_dtype,
|
|
19
|
+
bnb_4bit_use_double_quant=True,
|
|
20
|
+
)
|
|
21
|
+
model = AutoModelForCausalLM.from_pretrained(
|
|
22
|
+
pretrained_model_name_or_path=config.model_name,
|
|
23
|
+
attn_implementation=config.model_attn_implementation,
|
|
24
|
+
quantization_config=bnb_config,
|
|
25
|
+
device_map="auto" # let bitsandbytes handle placement
|
|
26
|
+
)
|
|
27
|
+
model = prepare_model_for_kbit_training(model)
|
|
28
|
+
elif config.device == "mps":
|
|
29
|
+
model = AutoModelForCausalLM.from_pretrained(
|
|
30
|
+
pretrained_model_name_or_path=config.model_name,
|
|
31
|
+
attn_implementation=config.model_attn_implementation,
|
|
32
|
+
dtype=config.model_dtype
|
|
33
|
+
).to("mps")
|
|
34
|
+
else:
|
|
35
|
+
model = AutoModelForCausalLM.from_pretrained(
|
|
36
|
+
pretrained_model_name_or_path=config.model_name,
|
|
37
|
+
attn_implementation=config.model_attn_implementation,
|
|
38
|
+
dtype=config.model_dtype
|
|
39
|
+
).to("cpu")
|
|
40
|
+
|
|
41
|
+
logger.info(
|
|
42
|
+
f"Model: {config.model_name} | Device: {config.device} | "
|
|
43
|
+
f"Dtype: {config.model_dtype} | Attn: {config.model_attn_implementation}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# forces the input to require gradients, ensuring the backward pass graph stays connected when using frozen base models with gradient checkpointing
|
|
47
|
+
if config.trainer_gradient_checkpointing:
|
|
48
|
+
model.enable_input_require_grads()
|
|
49
|
+
|
|
50
|
+
lora_config = LoraConfig(
|
|
51
|
+
lora_alpha=config.lora_alpha,
|
|
52
|
+
lora_dropout=config.lora_dropout,
|
|
53
|
+
r=config.lora_r,
|
|
54
|
+
bias=config.lora_bias,
|
|
55
|
+
task_type="CAUSAL_LM",
|
|
56
|
+
target_modules=config.lora_target_modules,
|
|
57
|
+
)
|
|
58
|
+
lora_model = get_peft_model(model, lora_config)
|
|
59
|
+
return lora_model
|