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,136 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging.config
|
|
3
|
+
import shutil
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Tuple
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from datasets import Features, IterableDataset, Sequence, Value, load_dataset
|
|
9
|
+
from transformers import AutoTokenizer
|
|
10
|
+
|
|
11
|
+
from .config import Config
|
|
12
|
+
from .model import load_and_configure_lora_model
|
|
13
|
+
from .train import plot_loss, save_log, train_lora_model, merge_lora_and_save
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _setup_logger(log_level: str) -> None:
|
|
20
|
+
"""
|
|
21
|
+
Configure the root logger ("") to capture logs from the entry point (__main__),
|
|
22
|
+
all internal sub-modules, and third-party libraries via a single handler.
|
|
23
|
+
"""
|
|
24
|
+
logger_config = {
|
|
25
|
+
"version": 1,
|
|
26
|
+
"disable_existing_loggers": False,
|
|
27
|
+
"formatters": {
|
|
28
|
+
"standard": {
|
|
29
|
+
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"handlers": {
|
|
33
|
+
"stderr_handler": {
|
|
34
|
+
"level": log_level,
|
|
35
|
+
"class": "logging.StreamHandler",
|
|
36
|
+
"formatter": "standard",
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"loggers": {
|
|
40
|
+
"": { # "" corresponds to root logger
|
|
41
|
+
"handlers": ["stderr_handler"],
|
|
42
|
+
"level": log_level,
|
|
43
|
+
"propagate": False,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
logging.config.dictConfig(logger_config)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _parse_args() -> argparse.Namespace:
|
|
51
|
+
parser = argparse.ArgumentParser(description="Start or resume LoRA model training")
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"--config",
|
|
54
|
+
type=Path,
|
|
55
|
+
required=True,
|
|
56
|
+
help="YAML config file path with 'finetune:' section. All Config.MISSING fields must be provided."
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--log-level",
|
|
60
|
+
type=str,
|
|
61
|
+
default="INFO",
|
|
62
|
+
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
|
63
|
+
help="Logging level.",
|
|
64
|
+
)
|
|
65
|
+
args = parser.parse_args()
|
|
66
|
+
return args
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _ensure_clean_checkpoint_dir(config: Config) -> None:
|
|
70
|
+
checkpoints_dir = config.trainer_checkpoints_dir_path
|
|
71
|
+
checkpoint = config.trainer_resume_from_checkpoint
|
|
72
|
+
clear = config.trainer_clear_checkpoint_dir
|
|
73
|
+
if checkpoint and not clear:
|
|
74
|
+
logger.info(f"Resuming from: {checkpoint}")
|
|
75
|
+
elif not checkpoint and clear:
|
|
76
|
+
if checkpoints_dir.exists():
|
|
77
|
+
logger.warning(f"Deleting: {checkpoints_dir}")
|
|
78
|
+
shutil.rmtree(checkpoints_dir)
|
|
79
|
+
checkpoints_dir.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
logger.info(f"Cleared: {checkpoints_dir}")
|
|
81
|
+
elif checkpoint and clear:
|
|
82
|
+
raise RuntimeError("Configuration conflict: Cannot resume from a checkpoint while '--delete-all-checkpoints' is set.")
|
|
83
|
+
else:
|
|
84
|
+
logger.info(f"Starting fresh training. Existing checkpoints in {checkpoints_dir} are preserved.")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def load_datasets(config: Config) -> Tuple[IterableDataset, IterableDataset]:
|
|
88
|
+
# Define the expected schema/features of datasets.
|
|
89
|
+
# Use 'int32' which the datasets library and pytorch map correctly to int tensors.
|
|
90
|
+
dataset_features = Features({
|
|
91
|
+
'input_ids': Sequence(feature=Value(dtype='int32')),
|
|
92
|
+
'attention_mask': Sequence(feature=Value(dtype='int32')),
|
|
93
|
+
'labels': Sequence(feature=Value(dtype='int32')),
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
# Enable streaming mode to load the dataset as an iterator.
|
|
97
|
+
# This allows processing data samples on-the-fly without downloading or loading the entire dataset into memory.
|
|
98
|
+
# https://huggingface.co/docs/datasets/stream
|
|
99
|
+
train_dataset = load_dataset("json", data_files=str(config.train_dataset_path), features=dataset_features, streaming=True)["train"]
|
|
100
|
+
train_dataset = train_dataset.shuffle(buffer_size=config.dataset_shuffle_buffer_size, seed=config.dataset_shuffle_seed) # take up to suffle_buffer_size examples and randomly shuffle them
|
|
101
|
+
eval_dataset = load_dataset("json", data_files=str(config.eval_dataset_path), features=dataset_features, streaming=True)["train"]
|
|
102
|
+
return train_dataset, eval_dataset
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def run(config: Config) -> None:
|
|
106
|
+
_ensure_clean_checkpoint_dir(config)
|
|
107
|
+
|
|
108
|
+
train_dataset, eval_dataset= load_datasets(config)
|
|
109
|
+
logger.info(f"Dataset: {config.dataset_train_dataset_length} train examples, max_steps={config.trainer_max_steps}")
|
|
110
|
+
|
|
111
|
+
lora_model = load_and_configure_lora_model(config)
|
|
112
|
+
lora_model.print_trainable_parameters()
|
|
113
|
+
|
|
114
|
+
tokenizer = AutoTokenizer.from_pretrained(config.model_name)
|
|
115
|
+
tokenizer.pad_token = config.fim_pad_token
|
|
116
|
+
tokenizer.padding_side = "right"
|
|
117
|
+
|
|
118
|
+
log_history = train_lora_model(config, lora_model, tokenizer, train_dataset, eval_dataset)
|
|
119
|
+
merge_lora_and_save(config, tokenizer)
|
|
120
|
+
save_log(config, log_history)
|
|
121
|
+
|
|
122
|
+
plot_loss(config)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def main() -> None:
|
|
126
|
+
user_args = _parse_args()
|
|
127
|
+
_setup_logger(user_args.log_level)
|
|
128
|
+
try:
|
|
129
|
+
finetune_config = Config.load_from_yaml(user_args.config)
|
|
130
|
+
run(finetune_config)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
logger.exception(f"Finetuning failed")
|
|
133
|
+
sys.exit(1)
|
|
134
|
+
|
|
135
|
+
if __name__ == "__main__":
|
|
136
|
+
main()
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import gc
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, List
|
|
7
|
+
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
import torch
|
|
10
|
+
from datasets import IterableDataset
|
|
11
|
+
from peft import PeftModel
|
|
12
|
+
from transformers import AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments
|
|
13
|
+
|
|
14
|
+
from .config import Config
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FIMDataCollator:
|
|
21
|
+
def __init__(self, tokenizer, label_pad_token_id):
|
|
22
|
+
self.tokenizer = tokenizer
|
|
23
|
+
self.label_pad_token_id = label_pad_token_id
|
|
24
|
+
|
|
25
|
+
def __call__(self, examples: List[Dict[str, List[int]]]) -> Dict[str, torch.Tensor]:
|
|
26
|
+
# find max length for this specific batch
|
|
27
|
+
max_ex_length = 0
|
|
28
|
+
for ex in examples:
|
|
29
|
+
if len(ex["input_ids"]) > max_ex_length:
|
|
30
|
+
max_ex_length = len(ex["input_ids"])
|
|
31
|
+
|
|
32
|
+
# apply padding
|
|
33
|
+
padded_examples = []
|
|
34
|
+
for ex in examples:
|
|
35
|
+
pad_length = max_ex_length - len(ex["input_ids"])
|
|
36
|
+
padded_ex = {
|
|
37
|
+
"input_ids": (ex["input_ids"] + [self.tokenizer.pad_token_id] * pad_length),
|
|
38
|
+
"attention_mask": (ex["attention_mask"] + [0] * pad_length),
|
|
39
|
+
"labels": (ex["labels"] + [self.label_pad_token_id] * pad_length)
|
|
40
|
+
}
|
|
41
|
+
padded_examples.append(padded_ex)
|
|
42
|
+
|
|
43
|
+
# stack separate examples into a single tensors matrix
|
|
44
|
+
examples_batch = {
|
|
45
|
+
"input_ids": torch.tensor([ex["input_ids"] for ex in padded_examples], dtype=torch.long),
|
|
46
|
+
"attention_mask": torch.tensor([ex["attention_mask"] for ex in padded_examples], dtype=torch.long),
|
|
47
|
+
"labels": torch.tensor([ex["labels"] for ex in padded_examples], dtype=torch.long)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return examples_batch
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def train_lora_model(
|
|
54
|
+
config: Config,
|
|
55
|
+
lora_model: PeftModel,
|
|
56
|
+
tokenizer: AutoTokenizer,
|
|
57
|
+
train_dataset: IterableDataset,
|
|
58
|
+
eval_dataset: IterableDataset,
|
|
59
|
+
) -> List:
|
|
60
|
+
logger.info(f"Starting training: {config.trainer_max_steps} steps on {config.device}, batch_size={config.trainer_per_device_train_batch_size}")
|
|
61
|
+
|
|
62
|
+
if config.model_dtype == torch.bfloat16:
|
|
63
|
+
trainer_bf16 = True
|
|
64
|
+
trainer_fp16 = False
|
|
65
|
+
elif config.model_dtype == torch.float16:
|
|
66
|
+
trainer_bf16 = False
|
|
67
|
+
trainer_fp16 = True
|
|
68
|
+
else:
|
|
69
|
+
trainer_bf16 = False
|
|
70
|
+
trainer_fp16 = False
|
|
71
|
+
|
|
72
|
+
training_args = TrainingArguments(
|
|
73
|
+
output_dir=config.trainer_checkpoints_dir_path,
|
|
74
|
+
per_device_train_batch_size=config.trainer_per_device_train_batch_size,
|
|
75
|
+
per_device_eval_batch_size=config.trainer_per_device_eval_batch_size,
|
|
76
|
+
gradient_accumulation_steps=config.trainer_gradient_accumulation_steps,
|
|
77
|
+
learning_rate=config.trainer_learning_rate,
|
|
78
|
+
weight_decay=config.trainer_weight_decay,
|
|
79
|
+
max_grad_norm=config.trainer_max_grad_norm,
|
|
80
|
+
max_steps=config.trainer_max_steps,
|
|
81
|
+
lr_scheduler_type=config.trainer_lr_scheduler_type,
|
|
82
|
+
warmup_steps=config.trainer_warmup_steps,
|
|
83
|
+
eval_strategy=config.trainer_eval_strategy,
|
|
84
|
+
logging_steps=config.trainer_logging_steps,
|
|
85
|
+
eval_steps=config.trainer_eval_steps,
|
|
86
|
+
logging_strategy=config.trainer_logging_strategy,
|
|
87
|
+
save_strategy=config.trainer_save_strategy,
|
|
88
|
+
save_steps=config.trainer_save_steps,
|
|
89
|
+
bf16=trainer_bf16,
|
|
90
|
+
fp16=trainer_fp16,
|
|
91
|
+
gradient_checkpointing=config.trainer_gradient_checkpointing
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
data_collator = FIMDataCollator(
|
|
95
|
+
tokenizer=tokenizer,
|
|
96
|
+
label_pad_token_id=config.label_pad_token_id
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
trainer = Trainer(
|
|
100
|
+
model=lora_model,
|
|
101
|
+
args=training_args,
|
|
102
|
+
train_dataset=train_dataset,
|
|
103
|
+
eval_dataset=eval_dataset,
|
|
104
|
+
processing_class = tokenizer,
|
|
105
|
+
data_collator = data_collator
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
checkpoint = config.trainer_resume_from_checkpoint
|
|
109
|
+
if checkpoint == "last":
|
|
110
|
+
trainer.train(resume_from_checkpoint=True)
|
|
111
|
+
elif checkpoint is not None:
|
|
112
|
+
trainer.train(resume_from_checkpoint=checkpoint)
|
|
113
|
+
else:
|
|
114
|
+
trainer.train() # train from scratch
|
|
115
|
+
|
|
116
|
+
lora_model.save_pretrained(config.lora_adapter_path) # save lora adapter only
|
|
117
|
+
log_history = trainer.state.log_history
|
|
118
|
+
|
|
119
|
+
del trainer
|
|
120
|
+
del lora_model
|
|
121
|
+
return log_history
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def merge_lora_and_save(config: Config, tokenizer: AutoTokenizer) -> None:
|
|
125
|
+
gc.collect() # force garbage collection
|
|
126
|
+
|
|
127
|
+
if torch.cuda.is_available():
|
|
128
|
+
torch.cuda.synchronize() # wait for gpu
|
|
129
|
+
torch.cuda.empty_cache() # clear gpu cache
|
|
130
|
+
|
|
131
|
+
# ensure offload folder for merging exists before loading the model
|
|
132
|
+
config.trainer_model_merge_offload_folder_path.mkdir(parents=True, exist_ok=True)
|
|
133
|
+
|
|
134
|
+
# load fresh base model
|
|
135
|
+
base_model = AutoModelForCausalLM.from_pretrained(
|
|
136
|
+
pretrained_model_name_or_path=config.model_name,
|
|
137
|
+
dtype=config.model_dtype,
|
|
138
|
+
device_map="auto",
|
|
139
|
+
offload_folder=str(config.trainer_model_merge_offload_folder_path), # offload model layers to disk during loading to prevent RAM (OOM) crashes
|
|
140
|
+
low_cpu_mem_usage=True
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
lora_model = PeftModel.from_pretrained(
|
|
144
|
+
model=base_model,
|
|
145
|
+
model_id=config.lora_adapter_path,
|
|
146
|
+
offload_folder=str(config.trainer_model_merge_offload_folder_path)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# merge lora adapter into base model and save it with the tokenizer of the model
|
|
150
|
+
merged_model= lora_model.merge_and_unload()
|
|
151
|
+
merged_model = merged_model.to(config.model_dtype)
|
|
152
|
+
merged_model.save_pretrained(config.lora_model_path)
|
|
153
|
+
tokenizer.save_pretrained(config.lora_model_path)
|
|
154
|
+
|
|
155
|
+
# clean up offload folder
|
|
156
|
+
if config.trainer_model_merge_offload_folder_path.exists():
|
|
157
|
+
shutil.rmtree(config.trainer_model_merge_offload_folder_path)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def save_log(config: Config, log_history: List) -> None:
|
|
161
|
+
history = {
|
|
162
|
+
"train": {"steps": [], "loss": [], "learning_rate": [], "epoch": []},
|
|
163
|
+
"eval": {"steps": [], "loss": [], "epoch": []}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for entry in log_history:
|
|
167
|
+
# Training logs
|
|
168
|
+
if "loss" in entry:
|
|
169
|
+
history["train"]["loss"].append(entry["loss"])
|
|
170
|
+
history["train"]["steps"].append(entry["step"])
|
|
171
|
+
history["train"]["epoch"].append(entry.get("epoch"))
|
|
172
|
+
history["train"]["learning_rate"].append(entry.get("learning_rate"))
|
|
173
|
+
|
|
174
|
+
# Evaluation logs
|
|
175
|
+
elif "eval_loss" in entry:
|
|
176
|
+
history["eval"]["loss"].append(entry["eval_loss"])
|
|
177
|
+
history["eval"]["steps"].append(entry["step"])
|
|
178
|
+
history["eval"]["epoch"].append(entry.get("epoch"))
|
|
179
|
+
|
|
180
|
+
log_path = config.trainer_log_path
|
|
181
|
+
with log_path.open("w", encoding="utf-8") as f:
|
|
182
|
+
json.dump(history, f, indent=2)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def plot_loss(config: Config) -> None:
|
|
186
|
+
log_path = config.trainer_log_path
|
|
187
|
+
with log_path.open("r", encoding="utf-8") as f:
|
|
188
|
+
data = json.load(f)
|
|
189
|
+
|
|
190
|
+
plt.figure(figsize=(8, 5))
|
|
191
|
+
|
|
192
|
+
if data["train"]["steps"]:
|
|
193
|
+
plt.plot(data["train"]["steps"], data["train"]["loss"], label="Train Loss")
|
|
194
|
+
|
|
195
|
+
if data["eval"]["steps"]:
|
|
196
|
+
plt.plot(data["eval"]["steps"], data["eval"]["loss"], label="Eval Loss", marker='o')
|
|
197
|
+
|
|
198
|
+
plt.xlabel("Step")
|
|
199
|
+
plt.ylabel("Loss")
|
|
200
|
+
plt.title("Training and Evaluation Loss")
|
|
201
|
+
plt.legend()
|
|
202
|
+
plt.grid(True)
|
|
203
|
+
plt.savefig(config.trainer_plot_path)
|
|
204
|
+
plt.close()
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import logging.config
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .preprocess.config import Config as PreprocessConfig
|
|
8
|
+
from .preprocess.run import run as preprocess_run
|
|
9
|
+
from .finetune.config import Config as FinetuneConfig
|
|
10
|
+
from .finetune.run import run as finetune_run
|
|
11
|
+
from .evaluate.config import Config as EvaluateConfig
|
|
12
|
+
from .evaluate.run import run as evaluate_run
|
|
13
|
+
from .convert.config import Config as ConvertConfig
|
|
14
|
+
from .convert.run import run as convert_run
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _setup_logger(log_level: str) -> None:
|
|
21
|
+
"""
|
|
22
|
+
Configure the root logger ("") to capture logs from the entry point (__main__),
|
|
23
|
+
all internal sub-modules, and third-party libraries via a single handler.
|
|
24
|
+
"""
|
|
25
|
+
logger_config = {
|
|
26
|
+
"version": 1,
|
|
27
|
+
"disable_existing_loggers": False,
|
|
28
|
+
"formatters": {
|
|
29
|
+
"standard": {
|
|
30
|
+
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"handlers": {
|
|
34
|
+
"stderr_handler": {
|
|
35
|
+
"level": log_level,
|
|
36
|
+
"class": "logging.StreamHandler",
|
|
37
|
+
"formatter": "standard",
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"loggers": {
|
|
41
|
+
"": { # "" corresponds to root logger
|
|
42
|
+
"handlers": ["stderr_handler"],
|
|
43
|
+
"level": log_level,
|
|
44
|
+
"propagate": False,
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
logging.config.dictConfig(logger_config)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _parse_args() -> argparse.Namespace:
|
|
52
|
+
parser = argparse.ArgumentParser(
|
|
53
|
+
description="Run full pipeline: preprocess -> finetune -> evaluate."
|
|
54
|
+
)
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
"--config",
|
|
57
|
+
type=Path,
|
|
58
|
+
required=True,
|
|
59
|
+
help="YAML config file path with 'preprocess:', 'finetune:' and 'evaluate:' sections.",
|
|
60
|
+
)
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"--log-level",
|
|
63
|
+
type=str,
|
|
64
|
+
default="INFO",
|
|
65
|
+
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
|
66
|
+
help="Logging level for the pipeline entrypoint.",
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"--skip-preprocess",
|
|
70
|
+
action="store_true",
|
|
71
|
+
help="Skip preprocess stage and reuse existing datasets.",
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"--skip-finetune",
|
|
75
|
+
action="store_true",
|
|
76
|
+
help="Skip finetune stage and reuse existing checkpoints.",
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--skip-evaluate",
|
|
80
|
+
action="store_true",
|
|
81
|
+
help="Skip evaluation stage.",
|
|
82
|
+
)
|
|
83
|
+
parser.add_argument(
|
|
84
|
+
"--skip-convert",
|
|
85
|
+
action="store_true",
|
|
86
|
+
help="Skip conversion to gguf file stage.",
|
|
87
|
+
)
|
|
88
|
+
return parser.parse_args()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def run_pipeline(
|
|
92
|
+
config_path: Path,
|
|
93
|
+
skip_preprocess: bool = False,
|
|
94
|
+
skip_finetune: bool = False,
|
|
95
|
+
skip_evaluate: bool = False,
|
|
96
|
+
skip_convert: bool = False,
|
|
97
|
+
) -> None:
|
|
98
|
+
logger.info("Starting end-to-end pipeline")
|
|
99
|
+
|
|
100
|
+
if not skip_preprocess:
|
|
101
|
+
logger.info("=== Stage 1/4: Preprocess ===")
|
|
102
|
+
preprocess_config = PreprocessConfig.load_from_yaml(config_path)
|
|
103
|
+
preprocess_run(preprocess_config)
|
|
104
|
+
logger.info("Finished preprocess stage")
|
|
105
|
+
else:
|
|
106
|
+
logger.info("Skipping preprocess stage")
|
|
107
|
+
|
|
108
|
+
if not skip_finetune:
|
|
109
|
+
logger.info("=== Stage 2/4: Finetune ===")
|
|
110
|
+
finetune_config = FinetuneConfig.load_from_yaml(config_path)
|
|
111
|
+
finetune_run(finetune_config)
|
|
112
|
+
logger.info("Finished finetune stage")
|
|
113
|
+
else:
|
|
114
|
+
logger.info("Skipping finetune stage")
|
|
115
|
+
|
|
116
|
+
if not skip_evaluate:
|
|
117
|
+
logger.info("=== Stage 3/4: Evaluate ===")
|
|
118
|
+
evaluate_config = EvaluateConfig.load_from_yaml(config_path)
|
|
119
|
+
evaluate_run(evaluate_config)
|
|
120
|
+
logger.info("Finished evaluate stage")
|
|
121
|
+
else:
|
|
122
|
+
logger.info("Skipping evaluate stage")
|
|
123
|
+
|
|
124
|
+
if not skip_convert:
|
|
125
|
+
logger.info("=== Stage 4/4: Convert ===")
|
|
126
|
+
convert_config = ConvertConfig.load_from_yaml(config_path)
|
|
127
|
+
convert_run(convert_config)
|
|
128
|
+
logger.info("Finished conversion stage")
|
|
129
|
+
else:
|
|
130
|
+
logger.info("Skipping conversion stage")
|
|
131
|
+
|
|
132
|
+
logger.info("Pipeline completed successfully")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main() -> None:
|
|
136
|
+
user_args = _parse_args()
|
|
137
|
+
_setup_logger(user_args.log_level)
|
|
138
|
+
try:
|
|
139
|
+
run_pipeline(
|
|
140
|
+
config_path=user_args.config,
|
|
141
|
+
skip_preprocess=user_args.skip_preprocess,
|
|
142
|
+
skip_finetune=user_args.skip_finetune,
|
|
143
|
+
skip_evaluate=user_args.skip_evaluate,
|
|
144
|
+
skip_convert=user_args.skip_convert,
|
|
145
|
+
)
|
|
146
|
+
except Exception:
|
|
147
|
+
logger.exception("Pipeline execution failed")
|
|
148
|
+
sys.exit(1)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import math
|
|
3
|
+
import json
|
|
4
|
+
from importlib import resources
|
|
5
|
+
from dataclasses import dataclass, field, fields
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from omegaconf import OmegaConf, MISSING
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Config:
|
|
18
|
+
# --- Mandatory Parameters ---
|
|
19
|
+
model_name: str = MISSING
|
|
20
|
+
fim_prefix_token: str = MISSING
|
|
21
|
+
fim_middle_token: str = MISSING
|
|
22
|
+
fim_suffix_token: str = MISSING
|
|
23
|
+
fim_pad_token: str = MISSING
|
|
24
|
+
eos_token: str = MISSING
|
|
25
|
+
data_language: str = MISSING
|
|
26
|
+
data_extensions: list[str] = MISSING
|
|
27
|
+
|
|
28
|
+
# --- Preprocess Local Parameters ---
|
|
29
|
+
split_mode: str = "auto"
|
|
30
|
+
train_ratio: float = 0.8
|
|
31
|
+
eval_ratio: float = 0.1
|
|
32
|
+
test_ratio: float = 0.1
|
|
33
|
+
max_token_sequence_length: int = 1024 # used with bytes_per_token_ratio to convert bytes to tokens, final token count is thus not exact
|
|
34
|
+
max_code_blocks_ast_depth: int = 2 # depth 1 is root, 2 includes child nodes (e.g. functions)
|
|
35
|
+
min_middle_tokens_length: int = 20 # used with estimated bytes_per_token_ratio to convert bytes to tokens, final token count is thus not exact
|
|
36
|
+
max_middle_tokens_length: int = 200 # used with estimated bytes_per_token_ratio to convert bytes to tokens, final token count is thus not exact
|
|
37
|
+
fim_examples_per_subblock_ratio: float = 1.0 # 1.0 = all fim examples of a subblock are extracted, 0.5 = onls 50% of fim examples of a subblock are extracted
|
|
38
|
+
tokenizer_batch_size: int = 32
|
|
39
|
+
rng_seed: int = 0
|
|
40
|
+
|
|
41
|
+
# --- Tree Sitter Parser ---
|
|
42
|
+
tree_sitter_parser: Any = field(init=False) # type hint Any because omegaconf does not recognize ts.Parser as a valid type (yaml file cannot contain an object of ts.Parser)
|
|
43
|
+
tree_sitter_block_types: Any = field(init=False) # type hint Any because omegaconf does not support set type
|
|
44
|
+
tree_sitter_subblock_types: Any = field(init=False) # type hint Any because omegaconf does not support set type
|
|
45
|
+
|
|
46
|
+
# --- Paths ---
|
|
47
|
+
workspace_path: Path | None = None
|
|
48
|
+
raw_data_path: Path | None = None
|
|
49
|
+
train_dataset_path: Path = field(init=False)
|
|
50
|
+
eval_dataset_path: Path = field(init=False)
|
|
51
|
+
test_dataset_path: Path = field(init=False)
|
|
52
|
+
tree_sitter_parser_path: Path | None = None
|
|
53
|
+
tree_sitter_definitions_path: Path | None = None
|
|
54
|
+
|
|
55
|
+
# --- Randomization ---
|
|
56
|
+
rng: Any = field(init=False) # type hint Any because omegaconf does not support np.random.Generator type
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def load_from_yaml(cls, yaml_path: Path) -> "Config":
|
|
60
|
+
if not yaml_path.exists():
|
|
61
|
+
raise FileNotFoundError(f"Configuration file not found at: {yaml_path}")
|
|
62
|
+
|
|
63
|
+
config_dict = OmegaConf.structured(cls)
|
|
64
|
+
try:
|
|
65
|
+
yaml_file_node = OmegaConf.load(yaml_path)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
raise ValueError(f"Failed to parse YAML file: {yaml_path}") from e
|
|
68
|
+
|
|
69
|
+
yaml_file_dict = OmegaConf.to_container(yaml_file_node, resolve=True)
|
|
70
|
+
yaml_preprocess_dict = yaml_file_dict.get("preprocess", {})
|
|
71
|
+
|
|
72
|
+
yaml_preprocess_valid_dict = {}
|
|
73
|
+
# Filter YAML fields to include only those defined in the Config dataclass.
|
|
74
|
+
# This prevents OmegaConf from raising an AttributeError when encountering
|
|
75
|
+
# global YAML anchors or keys not present in the current Config dataclass.
|
|
76
|
+
for field in fields(cls):
|
|
77
|
+
if field.name in yaml_preprocess_dict:
|
|
78
|
+
yaml_preprocess_valid_dict[field.name] = yaml_preprocess_dict[field.name]
|
|
79
|
+
logger.debug(f"Filtered YAML configuration: {yaml_preprocess_valid_dict}")
|
|
80
|
+
|
|
81
|
+
merged_config_dict = OmegaConf.merge(config_dict, yaml_preprocess_valid_dict)
|
|
82
|
+
|
|
83
|
+
return OmegaConf.to_object(merged_config_dict)
|
|
84
|
+
|
|
85
|
+
def __post_init__(self) -> None:
|
|
86
|
+
self._validate_ratio()
|
|
87
|
+
self._setup_paths()
|
|
88
|
+
self._ensure_output_paths_exist()
|
|
89
|
+
self._load_language_blocks()
|
|
90
|
+
self._init_tree_sitter_parser()
|
|
91
|
+
self.rng = np.random.default_rng(seed=self.rng_seed)
|
|
92
|
+
logger.debug("Config initialization complete.")
|
|
93
|
+
|
|
94
|
+
def _validate_ratio(self):
|
|
95
|
+
total_ratio = self.train_ratio + self.eval_ratio + self.test_ratio
|
|
96
|
+
if not math.isclose(total_ratio, 1.0, rel_tol=1e-6):
|
|
97
|
+
raise ValueError(f"Train + eval + test ratios must sum to 1.0, got {total_ratio}")
|
|
98
|
+
|
|
99
|
+
def _setup_paths(self) -> None:
|
|
100
|
+
if self.workspace_path is None:
|
|
101
|
+
self.workspace_path = Path.cwd()
|
|
102
|
+
if self.raw_data_path is None:
|
|
103
|
+
self.raw_data_path = self.workspace_path / "data"
|
|
104
|
+
|
|
105
|
+
if self.tree_sitter_definitions_path is None:
|
|
106
|
+
try:
|
|
107
|
+
self.tree_sitter_definitions_path = Path(resources.files(__package__)/ "tree_sitter_definitions.json")
|
|
108
|
+
except Exception:
|
|
109
|
+
raise FileNotFoundError("Missing internal tree_sitter_definitions.json")
|
|
110
|
+
|
|
111
|
+
self.preprocess_outputs_dir_path = self.workspace_path / "outputs" / "preprocess"
|
|
112
|
+
self.train_dataset_path = self.preprocess_outputs_dir_path / "results" / "datasets" / "train_dataset.jsonl"
|
|
113
|
+
self.eval_dataset_path = self.preprocess_outputs_dir_path / "results" / "datasets" / "eval_dataset.jsonl"
|
|
114
|
+
self.test_dataset_path = self.preprocess_outputs_dir_path / "results" / "datasets" / "test_dataset.jsonl"
|
|
115
|
+
logger.debug(f"Resolved workspace path to: {self.workspace_path}")
|
|
116
|
+
|
|
117
|
+
def _ensure_output_paths_exist(self) -> None:
|
|
118
|
+
paths = [
|
|
119
|
+
self.preprocess_outputs_dir_path,
|
|
120
|
+
self.train_dataset_path,
|
|
121
|
+
self.eval_dataset_path,
|
|
122
|
+
self.test_dataset_path
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
for path in paths:
|
|
126
|
+
if not path.parent.exists():
|
|
127
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
logger.debug(f"Created parent directory: {path.parent}")
|
|
129
|
+
else:
|
|
130
|
+
logger.debug(f"Parent directory already exists: {path.parent}")
|
|
131
|
+
|
|
132
|
+
def _load_language_blocks(self) -> None:
|
|
133
|
+
blocks_path = self.tree_sitter_definitions_path
|
|
134
|
+
with open(blocks_path, "r", encoding="utf-8") as f:
|
|
135
|
+
language_data = json.load(f)
|
|
136
|
+
|
|
137
|
+
language_blocks = language_data.get(self.data_language)
|
|
138
|
+
if language_blocks is None:
|
|
139
|
+
raise ValueError(f"Language '{self.data_language}' not found in {blocks_path}")
|
|
140
|
+
|
|
141
|
+
tree_sitter_block_types = language_blocks.get("block_types")
|
|
142
|
+
tree_sitter_subblock_types = language_blocks.get("subblock_types")
|
|
143
|
+
if not isinstance(tree_sitter_block_types, list) or not isinstance(tree_sitter_subblock_types, list):
|
|
144
|
+
raise ValueError(f"Invalid block definitions for '{self.data_language}' in {blocks_path}")
|
|
145
|
+
|
|
146
|
+
self.tree_sitter_block_types = set(tree_sitter_block_types)
|
|
147
|
+
self.tree_sitter_subblock_types = set(tree_sitter_subblock_types)
|
|
148
|
+
|
|
149
|
+
def _init_tree_sitter_parser(self) -> None:
|
|
150
|
+
if self.tree_sitter_parser_path:
|
|
151
|
+
logger.info(f"Loading custom tree-sitter parser from {self.tree_sitter_parser_path}")
|
|
152
|
+
from .extract import get_custom_tree_sitter_parser
|
|
153
|
+
self.tree_sitter_parser = get_custom_tree_sitter_parser(self.tree_sitter_parser_path, self.data_language)
|
|
154
|
+
else:
|
|
155
|
+
logger.info(f"Loading tree-sitter language pack parser {self.data_language}")
|
|
156
|
+
from .extract import get_tree_sitter_language_pack_parser
|
|
157
|
+
self.tree_sitter_parser = get_tree_sitter_language_pack_parser(self.data_language)
|
|
158
|
+
|
|
159
|
+
if self.tree_sitter_parser is None:
|
|
160
|
+
raise RuntimeError("Tree-sitter parser not initialized")
|