CoolFace
Apppublic

forestcalled/text-generation-webui

sourceHugging Faceupdated 3y agoView on Hugging Face
0likes
training.py777 linesDownload Raw Back to modules
1import os2 3os.environ["WANDB_MODE"] = "offline"4# os.environ["WANDB_DISABLED"] = "true"5 6import json7import math8import random9import shutil10import sys11import threading12import time13import traceback14from datetime import datetime15from pathlib import Path16 17import gradio as gr18import torch19import transformers20from datasets import Dataset, load_dataset21from peft import (22    LoraConfig,23    get_peft_model,24    prepare_model_for_kbit_training,25    set_peft_model_state_dict26)27from peft.utils.other import \28    TRANSFORMERS_MODELS_TO_LORA_TARGET_MODULES_MAPPING as model_to_lora_modules29from transformers import is_torch_xpu_available30from transformers.models.auto.modeling_auto import (31    MODEL_FOR_CAUSAL_LM_MAPPING_NAMES32)33 34from modules import shared, ui, utils35from modules.evaluate import (36    calculate_perplexity,37    generate_markdown_table,38    save_past_evaluations39)40from modules.logging_colors import logger41from modules.models import reload_model42from modules.utils import natural_keys43 44MODEL_CLASSES = {v[1]: v[0] for v in MODEL_FOR_CAUSAL_LM_MAPPING_NAMES.items()}45PARAMETERS = ["lora_name", "always_override", "q_proj_en", "v_proj_en", "k_proj_en", "o_proj_en", "gate_proj_en", "down_proj_en", "up_proj_en", "save_steps", "micro_batch_size", "batch_size", "epochs", "learning_rate", "lr_scheduler_type", "lora_rank", "lora_alpha", "lora_dropout", "cutoff_len", "dataset", "eval_dataset", "format", "eval_steps", "raw_text_file", "overlap_len", "newline_favor_len", "higher_rank_limit", "warmup_steps", "optimizer", "hard_cut_string", "train_only_after", "stop_at_loss", "add_eos_token", "min_chars", "report_to"]46WANT_INTERRUPT = False47 48train_log = {}49train_template = {}50 51 52def create_ui():53    mu = shared.args.multi_user54    with gr.Tab("Training", elem_id="training-tab"):55        with gr.Tab('Train LoRA', elem_id='lora-train-tab'):56            tmp = gr.State('')57            with gr.Row():58                with gr.Column():59                    gr.Markdown("[Tutorial](https://github.com/oobabooga/text-generation-webui/wiki/05-%E2%80%90-Training-Tab)")60 61                    with gr.Row():62                        copy_from = gr.Dropdown(label='Copy parameters from', value='None', choices=utils.get_available_loras(), elem_classes=['slim-dropdown'], interactive=not mu)63                        ui.create_refresh_button(copy_from, lambda: None, lambda: {'choices': utils.get_available_loras()}, 'refresh-button', interactive=not mu)64 65                    with gr.Row():66                        with gr.Column(scale=5):67                            lora_name = gr.Textbox(label='Name', info='The name of your new LoRA file')68                        with gr.Column():69                            always_override = gr.Checkbox(label='Override Existing Files', value=False, info='If the name is the same, checking will replace the existing file, and unchecking will load and continue from it (the rank must be the same).', elem_classes=['no-background'])70 71                    with gr.Accordion(label='Target Modules', open=False):72                        gr.Markdown("Selects which modules to target in training. Targeting more modules is closer to a full fine-tune at the cost of increased VRAM requirements and adapter size.\nNOTE: Only works for model_id='llama', other types will retain default training behavior and not use these settings.")73                        with gr.Row():74                            with gr.Column():75                                q_proj_en = gr.Checkbox(label='Enable q_proj', value=True)76                            with gr.Column():77                                v_proj_en = gr.Checkbox(label='Enable v_proj', value=True)78                            with gr.Column():79                                k_proj_en = gr.Checkbox(label='Enable k_proj', value=False)80                            with gr.Column():81                                o_proj_en = gr.Checkbox(label='Enable o_proj', value=False)82                            with gr.Column():83                                gate_proj_en = gr.Checkbox(label='Enable gate_proj', value=False)84                            with gr.Column():85                                down_proj_en = gr.Checkbox(label='Enable down_proj', value=False)86                            with gr.Column():87                                up_proj_en = gr.Checkbox(label='Enable up_proj', value=False)88 89                    with gr.Row():90                        with gr.Column():91                            lora_rank = gr.Slider(label='LoRA Rank', value=32, minimum=0, maximum=1024, step=4, info='Also called dimension count. Higher values = larger file, more content control. Smaller values = smaller file, less control. Use 4 or 8 for style, 128 or 256 to teach, 1024+ for fine-detail on big data. More VRAM is needed for higher ranks.')92                            lora_alpha = gr.Slider(label='LoRA Alpha', value=64, minimum=0, maximum=2048, step=4, info='This divided by the rank becomes the scaling of the LoRA. Higher means stronger. A good standard value is twice your Rank.')93                            batch_size = gr.Slider(label='Batch Size', value=128, minimum=0, maximum=1024, step=4, info='Global batch size. The two batch sizes together determine gradient accumulation (gradientAccum = batch / microBatch). Higher gradient accum values lead to better quality training.')94                            micro_batch_size = gr.Slider(label='Micro Batch Size', value=4, minimum=1, maximum=128, step=1, info='Per-device batch size (NOTE: multiple devices not yet implemented). Increasing this will increase VRAM usage.')95                            cutoff_len = gr.Slider(label='Cutoff Length', minimum=0, maximum=4096, value=256, step=32, info='Cutoff length for text input. Essentially, how long of a line of text to feed in at a time. Higher values require drastically more VRAM.')96 97                        with gr.Column():98                            save_steps = gr.Number(label='Save every n steps', value=0, info='If above 0, a checkpoint of the LoRA will be saved every time this many steps pass.')99 100                            epochs = gr.Number(label='Epochs', value=3, info='Number of times every entry in the dataset should be fed into training. So 1 means feed each item in once, 5 means feed it in five times, etc.')101                            learning_rate = gr.Textbox(label='Learning Rate', value='3e-4', info='In scientific notation. 3e-4 is a good starting base point. 1e-2 is extremely high, 1e-6 is extremely low.')102                            with gr.Row():103                                lr_scheduler_type = gr.Dropdown(label='LR Scheduler', value='linear', choices=['linear', 'constant', 'constant_with_warmup', 'cosine', 'cosine_with_restarts', 'polynomial', 'inverse_sqrt'], info='Learning rate scheduler - defines how the learning rate changes over time. "Constant" means never change, "linear" means to go in a straight line from the learning rate down to 0, cosine follows a curve, etc.', elem_classes=['slim-dropdown'])104 105                    with gr.Accordion(label='Advanced Options', open=False):106                        with gr.Row():107                            with gr.Column():108                                lora_dropout = gr.Slider(label='LoRA Dropout', minimum=0.0, maximum=1.0, step=0.025, value=0.05, info='Percentage probability for dropout of LoRA layers. This can help reduce overfitting. Most users should leave at default.')109                                stop_at_loss = gr.Slider(label='Stop at loss', minimum=0.0, maximum=3.0, step=0.1, value=0.00, info='The process will automatically stop once the desired loss value is reached. (reasonable numbers are 1.5-1.8)')110                                with gr.Row():111                                    optimizer = gr.Dropdown(label='Optimizer', value='adamw_torch', choices=['adamw_hf', 'adamw_torch', 'adamw_torch_fused', 'adamw_torch_xla', 'adamw_apex_fused', 'adafactor', 'adamw_bnb_8bit', 'adamw_anyprecision', 'sgd', 'adagrad'], info='Different optimizer implementation options, for advanced users. Effects of different options are not well documented yet.', elem_classes=['slim-dropdown'])112 113                            with gr.Column():114                                warmup_steps = gr.Number(label='Warmup Steps', value=100, info='For this many steps at the start, the learning rate will be lower than normal. This helps the trainer prepare the model and precompute statistics to improve the quality of training after the start.')115                                train_only_after = gr.Textbox(label='Train Only After', value='', info='Only consider text *after* this string in any given chunk for training. For Alpaca datasets, use "### Response:" to only train the response and ignore the input.')116 117                                add_eos_token = gr.Checkbox(label='Add EOS token', value=False, info="Adds EOS token for each dataset item. In case of raw text, the EOS will be added at the Hard Cut")118 119                                higher_rank_limit = gr.Checkbox(label='Enable higher ranks', value=False, info='If checked, changes Rank/Alpha slider above to go much higher. This will not work without a datacenter-class GPU.')120                                report_to = gr.Radio(label="Save detailed logs with", value="None", choices=["None", "wandb", "tensorboard"], interactive=True)121 122                with gr.Column():123                    with gr.Tab(label='Formatted Dataset'):124                        with gr.Row():125                            format = gr.Dropdown(choices=utils.get_datasets('training/formats', 'json'), value='None', label='Data Format', info='The format file used to decide how to format the dataset input.', elem_classes=['slim-dropdown'], interactive=not mu)126                            ui.create_refresh_button(format, lambda: None, lambda: {'choices': utils.get_datasets('training/formats', 'json')}, 'refresh-button', interactive=not mu)127 128                        with gr.Row():129                            dataset = gr.Dropdown(choices=utils.get_datasets('training/datasets', 'json'), value='None', label='Dataset', info='The dataset file to use for training.', elem_classes=['slim-dropdown'], interactive=not mu)130                            ui.create_refresh_button(dataset, lambda: None, lambda: {'choices': utils.get_datasets('training/datasets', 'json')}, 'refresh-button', interactive=not mu)131 132                        with gr.Row():133                            eval_dataset = gr.Dropdown(choices=utils.get_datasets('training/datasets', 'json'), value='None', label='Evaluation Dataset', info='The (optional) dataset file used to evaluate the model after training.', elem_classes=['slim-dropdown'], interactive=not mu)134                            ui.create_refresh_button(eval_dataset, lambda: None, lambda: {'choices': utils.get_datasets('training/datasets', 'json')}, 'refresh-button', interactive=not mu)135 136                        eval_steps = gr.Number(label='Evaluate every n steps', value=100, info='If an evaluation dataset is given, test it every time this many steps pass.')137 138                    with gr.Tab(label="Raw text file"):139                        with gr.Row():140                            raw_text_file = gr.Dropdown(choices=utils.get_datasets('training/datasets', 'txt'), value='None', label='Text file', info='The raw text file to use for training.', elem_classes=['slim-dropdown'], interactive=not mu)141                            ui.create_refresh_button(raw_text_file, lambda: None, lambda: {'choices': utils.get_datasets('training/datasets', 'txt')}, 'refresh-button', interactive=not mu)142 143                        with gr.Row():144                            with gr.Column():145                                overlap_len = gr.Slider(label='Overlap Length', minimum=0, maximum=512, value=128, step=16, info='How many tokens from the prior chunk of text to include into the next chunk. (The chunks themselves will be of a size determined by Cutoff Length). Setting overlap to exactly half the cutoff length may be ideal.')146                                newline_favor_len = gr.Slider(label='Prefer Newline Cut Length', minimum=0, maximum=512, value=128, step=16, info='Length (in characters, not tokens) of the maximum distance to shift an overlap cut by to ensure chunks cut at newlines. If too low, cuts may occur in the middle of lines.')147 148                            with gr.Column():149                                hard_cut_string = gr.Textbox(label='Hard Cut String', value='\\n\\n\\n', info='String that indicates a hard cut between text parts. Helps prevent unwanted overlap.')150                                min_chars = gr.Number(label='Ignore small blocks', value=0, info='Ignore Hard Cut blocks that have less or equal characters than this number')151 152                    with gr.Row():153                        start_button = gr.Button("Start LoRA Training", variant='primary', interactive=not mu)154                        stop_button = gr.Button("Interrupt", interactive=not mu)155 156                    output = gr.Markdown(value="Ready")157 158        with gr.Tab('Perplexity evaluation', elem_id='evaluate-tab'):159            with gr.Row():160                with gr.Column():161                    models = gr.Dropdown(utils.get_available_models(), label='Models', multiselect=True, interactive=not mu)162                    evaluate_text_file = gr.Dropdown(choices=['wikitext', 'ptb', 'ptb_new'] + utils.get_datasets('training/datasets', 'txt')[1:], value='wikitext', label='Input dataset', info='The raw text file on which the model will be evaluated. The first options are automatically downloaded: wikitext, ptb, and ptb_new. The next options are your local text files under training/datasets.', interactive=not mu)163                    with gr.Row():164                        with gr.Column():165                            stride_length = gr.Slider(label='Stride', minimum=0, maximum=32768, value=512, step=256, info='Used to make the evaluation faster at the cost of accuracy. 1 = slowest but most accurate. 512 is a common value.')166 167                        with gr.Column():168                            max_length = gr.Slider(label='max_length', minimum=0, maximum=shared.settings['truncation_length_max'], value=0, step=256, info='The context for each evaluation. If set to 0, the maximum context length for the model will be used.')169 170                    with gr.Row():171                        start_current_evaluation = gr.Button("Evaluate loaded model", interactive=not mu)172                        start_evaluation = gr.Button("Evaluate selected models", interactive=not mu)173                        stop_evaluation = gr.Button("Interrupt", interactive=not mu)174 175                with gr.Column():176                    evaluation_log = gr.Markdown(value='')177 178            evaluation_table = gr.Dataframe(value=generate_markdown_table(), interactive=True)179            with gr.Row():180                save_comments = gr.Button('Save comments', elem_classes="small-button", interactive=not mu)181                refresh_table = gr.Button('Refresh the table', elem_classes="small-button", interactive=not mu)182 183    # Training events184    all_params = [lora_name, always_override, q_proj_en, v_proj_en, k_proj_en, o_proj_en, gate_proj_en, down_proj_en, up_proj_en, save_steps, micro_batch_size, batch_size, epochs, learning_rate, lr_scheduler_type, lora_rank, lora_alpha, lora_dropout, cutoff_len, dataset, eval_dataset, format, eval_steps, raw_text_file, overlap_len, newline_favor_len, higher_rank_limit, warmup_steps, optimizer, hard_cut_string, train_only_after, stop_at_loss, add_eos_token, min_chars, report_to]185 186    copy_from.change(do_copy_params, [copy_from] + all_params, all_params)187    start_button.click(do_train, all_params, output)188    stop_button.click(do_interrupt, None, None, queue=False)189    higher_rank_limit.change(change_rank_limit, [higher_rank_limit], [lora_rank, lora_alpha])190 191    # Evaluation events. For some reason, the interrupt event192    # doesn't work with the .then() syntax, so I write them one193    # by one in this ugly but functional way.194    ev = start_evaluation.click(calculate_perplexity, [models, evaluate_text_file, stride_length, max_length], evaluation_log, show_progress=False)195    start_evaluation.click(generate_markdown_table, None, evaluation_table, show_progress=False)196 197    start_current_evaluation.click(lambda: ['current model'], None, tmp)198    ev_cur = start_current_evaluation.click(calculate_perplexity, [tmp, evaluate_text_file, stride_length, max_length], evaluation_log, show_progress=False)199    start_current_evaluation.click(generate_markdown_table, None, evaluation_table, show_progress=False)200 201    stop_evaluation.click(None, None, None, cancels=[ev, ev_cur], queue=False)202    refresh_table.click(generate_markdown_table, None, evaluation_table, show_progress=True)203    save_comments.click(204        save_past_evaluations, evaluation_table, None).then(205        lambda: "Comments saved.", None, evaluation_log, show_progress=False)206 207 208def do_interrupt():209    global WANT_INTERRUPT210    WANT_INTERRUPT = True211 212 213def do_copy_params(lora_name: str, *args):214    f_name = f"{shared.args.lora_dir}/{clean_path(None, lora_name)}/training_parameters.json"215    if Path(f_name).is_file():216        with open(f_name, 'r', encoding='utf-8') as format_file:217            params: dict[str, str] = json.load(format_file)218    else:219        params = {}220 221    result = list()222    for i in range(0, len(PARAMETERS)):223        key = PARAMETERS[i]224        if key in params:225            result.append(params[key])226        else:227            result.append(args[i])228 229    return result230 231 232def change_rank_limit(use_higher_ranks: bool):233    mult = 2 if use_higher_ranks else 1234    return {"maximum": 1024 * mult, "__type__": "update"}, {"maximum": 2048 * mult, "__type__": "update"}235 236 237def clean_path(base_path: str, path: str):238    """Strips unusual symbols and forcibly builds a path as relative to the intended directory."""239    path = path.replace('\\', '/').replace('..', '_')240    if base_path is None:241        return path242 243    return f'{Path(base_path).absolute()}/{path}'244 245 246def backup_adapter(input_folder):247    # Get the creation date of the file adapter_model.bin248    try:249        adapter_file = Path(f"{input_folder}/adapter_model.bin")250        if adapter_file.is_file():251 252            logger.info("Backing up existing LoRA adapter")253            creation_date = datetime.fromtimestamp(adapter_file.stat().st_ctime)254            creation_date_str = creation_date.strftime("Backup-%Y-%m-%d")255 256            # Create the new subfolder257            subfolder_path = Path(f"{input_folder}/{creation_date_str}")258            subfolder_path.mkdir(parents=True, exist_ok=True)259 260            # Check if the file already exists in the subfolder261            backup_adapter_file = Path(f"{input_folder}/{creation_date_str}/adapter_model.bin")262            if backup_adapter_file.is_file():263                print(" - Backup already exists. Skipping backup process.")264                return265 266            # Copy existing files to the new subfolder267            existing_files = Path(input_folder).iterdir()268            for file in existing_files:269                if file.is_file():270                    shutil.copy2(file, subfolder_path)271    except Exception as e:272        print("An error occurred in backup_adapter:", str(e))273 274 275def calc_trainable_parameters(model):276    trainable_params = 0277    all_param = 0278    for _, param in model.named_parameters():279        num_params = param.numel()280        # if using DS Zero 3 and the weights are initialized empty281        if num_params == 0 and hasattr(param, "ds_numel"):282            num_params = param.ds_numel283 284        all_param += num_params285        if param.requires_grad:286            trainable_params += num_params287 288    return trainable_params, all_param289 290 291def do_train(lora_name: str, always_override: bool, q_proj_en: bool, v_proj_en: bool, k_proj_en: bool, o_proj_en: bool, gate_proj_en: bool, down_proj_en: bool, up_proj_en: bool, save_steps: int, micro_batch_size: int, batch_size: int, epochs: int, learning_rate: str, lr_scheduler_type: str, lora_rank: int, lora_alpha: int, lora_dropout: float, cutoff_len: int, dataset: str, eval_dataset: str, format: str, eval_steps: int, raw_text_file: str, overlap_len: int, newline_favor_len: int, higher_rank_limit: bool, warmup_steps: int, optimizer: str, hard_cut_string: str, train_only_after: str, stop_at_loss: float, add_eos_token: bool, min_chars: int, report_to: str):292 293    if shared.args.monkey_patch:294        from alpaca_lora_4bit.monkeypatch.peft_tuners_lora_monkey_patch import (295            replace_peft_model_with_int4_lora_model296        )297        replace_peft_model_with_int4_lora_model()298 299    global WANT_INTERRUPT300    WANT_INTERRUPT = False301 302    # == Input validation / processing ==303    yield "Preparing the input..."304    lora_file_path = clean_path(None, lora_name)305    if lora_file_path.strip() == '':306        yield "Missing or invalid LoRA file name input."307        return308 309    lora_file_path = f"{Path(shared.args.lora_dir)}/{lora_file_path}"310    actual_lr = float(learning_rate)311    model_type = type(shared.model).__name__312 313    if model_type in MODEL_CLASSES:314        model_id = MODEL_CLASSES[model_type]315    else:316        model_id = "llama"317        if model_type == "PeftModelForCausalLM":318            if len(shared.lora_names) > 0:319                yield "You are trying to train a LoRA while you already have another LoRA loaded. This will work, but may have unexpected effects. *(Will continue anyway in 5 seconds, press `Interrupt` to stop.)*"320                logger.warning("Training LoRA over top of another LoRA. May have unexpected effects.")321            else:322                yield "Model ID not matched due to LoRA loading. Consider reloading base model. *(Will continue anyway in 5 seconds, press `Interrupt` to stop.)*"323                logger.warning("Model ID not matched due to LoRA loading. Consider reloading base model.")324        else:325            yield "LoRA training has only currently been validated for LLaMA, OPT, GPT-J, and GPT-NeoX models. Unexpected errors may follow. *(Will continue anyway in 5 seconds, press `Interrupt` to stop.)*"326            logger.warning(f"LoRA training has only currently been validated for LLaMA, OPT, GPT-J, and GPT-NeoX models. (Found model type: {model_type})")327 328        time.sleep(5)329 330    if shared.args.loader == 'GPTQ-for-LLaMa' and not shared.args.monkey_patch:331        yield "LoRA training with GPTQ-for-LLaMa requires loading with `--monkey-patch`"332        return333 334    if cutoff_len <= 0 or micro_batch_size <= 0 or batch_size <= 0 or actual_lr <= 0 or lora_rank <= 0 or lora_alpha <= 0:335        yield "Cannot input zeroes."336        return337 338    gradient_accumulation_steps = batch_size // micro_batch_size339    shared.tokenizer.pad_token_id = 0340    shared.tokenizer.padding_side = "left"341 342    # Populate target_modules list with chosen X_proj modules. Llama-based models only atm, non-llama will revert to default behavior.343    def list_target_modules(model_id):344        if model_id != "llama":345            return model_to_lora_modules[model_id]346 347        available_modules = {348            "gate": gate_proj_en,349            "down": down_proj_en,350            "up": up_proj_en,351            "q": q_proj_en,352            "v": v_proj_en,353            "k": k_proj_en,354            "o": o_proj_en,355        }356        target_mods = [f"{name}_proj" for name, enabled in available_modules.items() if enabled]357        return target_mods358 359    def encode(text, add_bos_token):360        result = shared.tokenizer.encode(text, truncation=True, max_length=cutoff_len)361        # Check if the first two tokens are BOS362        if len(result) >= 2 and result[:2] == [shared.tokenizer.bos_token_id, shared.tokenizer.bos_token_id]:363            result = result[1:]364 365        if not add_bos_token and result[0] == shared.tokenizer.bos_token_id:366            result = result[1:]367        return result368 369    def tokenize(prompt, append_eos_token=False):370 371        if train_only_after == '' or train_only_after not in prompt:372            input_ids = encode(prompt, True)373 374            if append_eos_token and input_ids[-1] != shared.tokenizer.eos_token_id and len(input_ids) < cutoff_len:375                input_ids.append(shared.tokenizer.eos_token_id)376 377            input_ids = [shared.tokenizer.pad_token_id] * (cutoff_len - len(input_ids)) + input_ids378            labels = [1] * len(input_ids)379 380        else:381            ind = prompt.index(train_only_after) + len(train_only_after)382            before_tokens = encode(prompt[:ind], True)383            after_tokens = encode(prompt[ind:], False)384 385            if append_eos_token and after_tokens[-1] != shared.tokenizer.eos_token_id:386                after_tokens.append(shared.tokenizer.eos_token_id)387 388            full_length = len(after_tokens) + len(before_tokens)389            if full_length > cutoff_len:390                after_tokens = after_tokens[:cutoff_len - len(before_tokens)]391            else:392                before_tokens = [shared.tokenizer.pad_token_id] * (cutoff_len - full_length) + before_tokens393 394            input_ids = before_tokens + after_tokens395            labels = [-100] * len(before_tokens) + [1] * len(after_tokens)396 397        input_ids = torch.tensor(input_ids)398        return {399            "input_ids": input_ids,400            "labels": labels,401            "attention_mask": input_ids.ne(shared.tokenizer.pad_token_id),402        }403 404    train_template.clear()405 406    # == Prep the dataset, format, etc ==407    if raw_text_file not in ['None', '']:408        train_template["template_type"] = "raw_text"409        logger.info("Loading raw text file dataset")410        fullpath = clean_path('training/datasets', f'{raw_text_file}')411        fullpath = Path(fullpath)412        if fullpath.is_dir():413            logger.info('Training path directory {}'.format(raw_text_file))414            raw_text = ""415            file_paths = sorted(fullpath.glob('*.txt'), key=lambda path: natural_keys(path.name))416            for file_path in file_paths:417                if file_path.is_file():418                    with file_path.open('r', encoding='utf-8') as file:419                        raw_text += file.read().replace('\r', '')420 421                    logger.info(f"Loaded training file: {file_path.name}")422        else:423            with open(clean_path('training/datasets', f'{raw_text_file}.txt'), 'r', encoding='utf-8') as file:424                raw_text = file.read().replace('\r', '')425 426        cut_string = hard_cut_string.replace('\\n', '\n')427        eos_added = 0428        out_tokens = []429        for text_part in raw_text.split(cut_string):430            if len(text_part.strip()) <= min_chars:431                continue432 433            tokens = shared.tokenizer.encode(text_part)434            if add_eos_token:435                tokens.append(shared.tokenizer.eos_token_id)436                eos_added += 1437 438            step = cutoff_len - overlap_len439            if step <= 0:440                yield f"Error: overlap_len ({overlap_len}) cannot be greater than or equal to cutoff_len ({cutoff_len})"441                return442 443            out_tokens.extend(split_chunks(tokens, cutoff_len, step))444 445        if eos_added > 0:446            print(f"EOS added to {eos_added} text blocks")447 448        del raw_text  # Note: could be a gig for a large dataset, so delete redundant data as we go to be safe on RAM449        text_chunks = [shared.tokenizer.decode(x) for x in out_tokens]450        del out_tokens451        if newline_favor_len > 0:452            text_chunks = [cut_chunk_for_newline(x, newline_favor_len) for x in text_chunks]453 454        train_data = Dataset.from_list([tokenize(x) for x in text_chunks])455        del text_chunks456        eval_data = None457    else:458        if dataset in ['None', '']:459            yield "Missing dataset choice input, cannot continue."460            return461 462        if format in ['None', '']:463            yield "Missing format choice input, cannot continue."464            return465 466        train_template["template_type"] = "dataset"467 468        with open(clean_path('training/formats', f'{format}.json'), 'r', encoding='utf-8-sig') as formatFile:469            format_data: dict[str, str] = json.load(formatFile)470 471        # == store training prompt ==472        for _, value in format_data.items():473            prompt_key = f"template_{len(train_template)}"474            train_template[prompt_key] = value475 476        def generate_prompt(data_point: dict[str, str]):477            for options, data in format_data.items():478                if set(options.split(',')) == set(x[0] for x in data_point.items() if (type(x[1]) is str and len(x[1].strip()) > 0)):479                    for key, val in data_point.items():480                        if type(val) is str:481                            data = data.replace(f'%{key}%', val)482                    return data483            raise RuntimeError(f'Data-point "{data_point}" has no keyset match within format "{list(format_data.keys())}"')484 485        def generate_and_tokenize_prompt(data_point):486            prompt = generate_prompt(data_point)487            return tokenize(prompt, add_eos_token)488 489        logger.info("Loading JSON datasets")490        data = load_dataset("json", data_files=clean_path('training/datasets', f'{dataset}.json'))491        train_data = data['train'].map(generate_and_tokenize_prompt, new_fingerprint='%030x' % random.randrange(16**30))492 493        if eval_dataset == 'None':494            eval_data = None495        else:496            eval_data = load_dataset("json", data_files=clean_path('training/datasets', f'{eval_dataset}.json'))497            eval_data = eval_data['train'].map(generate_and_tokenize_prompt, new_fingerprint='%030x' % random.randrange(16**30))498 499    # == We MUST reload model if it went through any previous training, even failed one ==500    if shared.model_dirty_from_training:501        selected_model = shared.model_name502        if selected_model:503            print("\033[1;31;1m(Model has been modified by previous training, it needs to be reloaded...)\033[0;37;0m")504            try:505                yield f"Reloading {selected_model}..."506                reload_model()507                if shared.model is not None:508                    print("Model reloaded OK, continue with training.")509                else:510                    return f"Failed to load {selected_model}."511            except:512                exc = traceback.format_exc()513                logger.error('Failed to reload the model.')514                print(exc)515                return exc.replace('\n', '\n\n')516 517    # == Start prepping the model itself ==518    if not hasattr(shared.model, 'lm_head') or hasattr(shared.model.lm_head, 'weight'):519        logger.info("Getting model ready")520        prepare_model_for_kbit_training(shared.model)521 522    # base model is now frozen and should not be reused for any other LoRA training than this one523    shared.model_dirty_from_training = True524 525    logger.info("Preparing for training")526    config = LoraConfig(527        r=lora_rank,528        lora_alpha=lora_alpha,529        target_modules=list_target_modules(model_id),530        lora_dropout=lora_dropout,531        bias="none",532        task_type="CAUSAL_LM"533    )534 535    # == Backup the existing adapter ==536    if not always_override:537        backup_adapter(lora_file_path)538 539    # == get model trainable params540    model_trainable_params, model_all_params = calc_trainable_parameters(shared.model)541 542    try:543        logger.info("Creating LoRA model")544        lora_model = get_peft_model(shared.model, config)545        if not always_override and Path(f"{lora_file_path}/adapter_model.bin").is_file():546            logger.info("Loading existing LoRA data")547            state_dict_peft = torch.load(f"{lora_file_path}/adapter_model.bin", weights_only=True)548            set_peft_model_state_dict(lora_model, state_dict_peft)549    except:550        yield traceback.format_exc().replace('\n', '\n\n')551        return552 553    if shared.args.monkey_patch:554        from alpaca_lora_4bit.autograd_4bit import Autograd4bitQuantLinear555        from alpaca_lora_4bit.models import Linear4bitLt556        for _, m in lora_model.named_modules():557            if isinstance(m, Autograd4bitQuantLinear) or isinstance(m, Linear4bitLt):558                if m.is_v1_model:559                    m.zeros = m.zeros.half()560                m.scales = m.scales.half()561 562    class Tracked():563        def __init__(self):564            self.current_steps = 0565            self.max_steps = 0566            self.did_save = False567 568    tracked = Tracked()569    actual_save_steps = math.ceil(save_steps / gradient_accumulation_steps)570 571    class Callbacks(transformers.TrainerCallback):572        def on_step_begin(self, args: transformers.TrainingArguments, state: transformers.TrainerState, control: transformers.TrainerControl, **kwargs):573            tracked.current_steps = state.global_step * gradient_accumulation_steps574            tracked.max_steps = state.max_steps * gradient_accumulation_steps575            if WANT_INTERRUPT:576                control.should_epoch_stop = True577                control.should_training_stop = True578            elif state.global_step > 0 and actual_save_steps > 0 and state.global_step % actual_save_steps == 0:579                lora_model.save_pretrained(f"{lora_file_path}/checkpoint-{tracked.current_steps}/")580                # Save log581                with open(f"{lora_file_path}/checkpoint-{tracked.current_steps}/training_log.json", 'w', encoding='utf-8') as file:582                    json.dump(train_log, file, indent=2)583                # == Save training prompt ==584                with open(f"{lora_file_path}/checkpoint-{tracked.current_steps}/training_prompt.json", 'w', encoding='utf-8') as file:585                    json.dump(train_template, file, indent=2)586 587        def on_substep_end(self, args: transformers.TrainingArguments, state: transformers.TrainerState, control: transformers.TrainerControl, **kwargs):588            tracked.current_steps += 1589            if WANT_INTERRUPT:590                control.should_epoch_stop = True591                control.should_training_stop = True592 593        def on_log(self, args: transformers.TrainingArguments, state: transformers.TrainerState, control: transformers.TrainerControl, logs, **kwargs):594            train_log.update(logs)595            train_log.update({"current_steps": tracked.current_steps})596            if WANT_INTERRUPT:597                print("\033[1;31;1mInterrupted by user\033[0;37;0m")598 599            print(f"\033[1;30;40mStep: {tracked.current_steps} \033[0;37;0m", end='')600            if 'loss' in logs:601                loss = float(logs['loss'])602                if loss <= stop_at_loss:603                    control.should_epoch_stop = True604                    control.should_training_stop = True605                    print(f"\033[1;31;1mStop Loss {stop_at_loss} reached.\033[0;37;0m")606 607    trainer = transformers.Trainer(608        model=lora_model,609        train_dataset=train_data,610        eval_dataset=eval_data,611        args=transformers.TrainingArguments(612            report_to=report_to if report_to != "None" else None,613            per_device_train_batch_size=micro_batch_size,614            gradient_accumulation_steps=gradient_accumulation_steps,615            warmup_steps=math.ceil(warmup_steps / gradient_accumulation_steps),616            num_train_epochs=epochs,617            learning_rate=actual_lr,618            fp16=False if shared.args.cpu else True,619            optim=optimizer,620            logging_steps=2 if stop_at_loss > 0 else 5,621            evaluation_strategy="steps" if eval_data is not None else "no",622            eval_steps=math.ceil(eval_steps / gradient_accumulation_steps) if eval_data is not None else None,623            save_strategy="steps" if eval_data is not None else "no",624            output_dir=lora_file_path,625            lr_scheduler_type=lr_scheduler_type,626            load_best_model_at_end=eval_data is not None,627            # TODO: Enable multi-device support628            ddp_find_unused_parameters=None,629            no_cuda=shared.args.cpu,630            use_ipex=True if is_torch_xpu_available and not shared.args.cpu else False631        ),632        data_collator=transformers.DataCollatorForLanguageModeling(shared.tokenizer, mlm=False),633        callbacks=list([Callbacks()])634    )635 636    lora_model.config.use_cache = False637 638    if torch.__version__ >= "2" and sys.platform != "win32":639        lora_model = torch.compile(lora_model)640 641    # == Save parameters for reuse ==642    with open(f"{lora_file_path}/training_parameters.json", 'w', encoding='utf-8') as file:643        vars = locals()644        json.dump({x: vars[x] for x in PARAMETERS}, file, indent=2)645 646    # == Save training prompt ==647    with open(f"{lora_file_path}/training_prompt.json", 'w', encoding='utf-8') as file:648        json.dump(train_template, file, indent=2)649 650    # == Main run and monitor loop ==651    logger.info("Starting training")652    yield "Starting..."653 654    lora_trainable_param, lora_all_param = calc_trainable_parameters(lora_model)655 656    projections_string = ", ".join([projection.replace("_proj", "") for projection in list_target_modules(model_id)])657 658    print(f"Training '{model_id}' model using ({projections_string}) projections")659 660    if lora_all_param > 0:661        print(f"Trainable params: {lora_trainable_param:,d} ({100 * lora_trainable_param / lora_all_param:.4f} %), All params: {lora_all_param:,d} (Model: {model_all_params:,d})")662 663    train_log.update({"base_model_name": shared.model_name})664    train_log.update({"base_model_class": shared.model.__class__.__name__})665    train_log.update({"base_loaded_in_4bit": getattr(lora_model, "is_loaded_in_4bit", False)})666    train_log.update({"base_loaded_in_8bit": getattr(lora_model, "is_loaded_in_8bit", False)})667    train_log.update({"projections": projections_string})668 669    if stop_at_loss > 0:670        print(f"Monitoring loss \033[1;31;1m(Auto-Stop at: {stop_at_loss})\033[0;37;0m")671 672    if WANT_INTERRUPT:673        yield "Interrupted before start."674        return675 676    def log_train_dataset(trainer):677        decoded_entries = []678        # Try to decode the entries and write the log file679        try:680            # Iterate over the first 10 elements in the dataset (or fewer if there are less than 10)681            for i in range(min(10, len(trainer.train_dataset))):682                decoded_text = shared.tokenizer.decode(trainer.train_dataset[i]['input_ids'])683                decoded_entries.append({"value": decoded_text})684 685            # Write the log file686            Path('logs').mkdir(exist_ok=True)687            with open(Path('logs/train_dataset_sample.json'), 'w') as json_file:688                json.dump(decoded_entries, json_file, indent=4)689 690            logger.info("Log file 'train_dataset_sample.json' created in the 'logs' directory.")691        except Exception as e:692            logger.error(f"Failed to create log file due to error: {e}")693 694    def threaded_run():695        log_train_dataset(trainer)696        trainer.train()697        # Note: save in the thread in case the gradio thread breaks (eg browser closed)698        lora_model.save_pretrained(lora_file_path)699        logger.info("LoRA training run is completed and saved.")700        # Save log701        with open(f"{lora_file_path}/training_log.json", 'w', encoding='utf-8') as file:702            json.dump(train_log, file, indent=2)703 704    thread = threading.Thread(target=threaded_run)705    thread.start()706    last_step = 0707    start_time = time.perf_counter()708 709    while thread.is_alive():710        time.sleep(0.5)711        if WANT_INTERRUPT:712            yield "Interrupting, please wait... *(Run will stop after the current training step completes.)*"713 714        elif tracked.current_steps != last_step:715            last_step = tracked.current_steps716            time_elapsed = time.perf_counter() - start_time717            if time_elapsed <= 0:718                timer_info = ""719                total_time_estimate = 999720            else:721                its = tracked.current_steps / time_elapsed722                if its > 1:723                    timer_info = f"`{its:.2f}` it/s"724                else:725                    timer_info = f"`{1.0/its:.2f}` s/it"726 727                total_time_estimate = (1.0 / its) * (tracked.max_steps)728 729            yield f"Running... **{tracked.current_steps}** / **{tracked.max_steps}** ... {timer_info}, {format_time(time_elapsed)} / {format_time(total_time_estimate)} ... {format_time(total_time_estimate - time_elapsed)} remaining"730 731    # Saving in the train thread might fail if an error occurs, so save here if so.732    if not tracked.did_save:733        logger.info("Training complete, saving")734        lora_model.save_pretrained(lora_file_path)735 736    if WANT_INTERRUPT:737        logger.info("Training interrupted.")738        yield f"Interrupted. Incomplete LoRA saved to `{lora_file_path}`."739    else:740        logger.info("Training complete!")741        yield f"Done! LoRA saved to `{lora_file_path}`.\n\nBefore testing your new LoRA, make sure to first reload the model, as it is currently dirty from training."742 743 744def split_chunks(arr, size, step):745    for i in range(0, len(arr), step):746        yield arr[i:i + size]747 748 749def cut_chunk_for_newline(chunk: str, max_length: int):750    if '\n' not in chunk:751        return chunk752 753    first_newline = chunk.index('\n')754    if first_newline < max_length:755        chunk = chunk[first_newline + 1:]756 757    if '\n' not in chunk:758        return chunk759 760    last_newline = chunk.rindex('\n')761    if len(chunk) - last_newline < max_length:762        chunk = chunk[:last_newline]763 764    return chunk765 766 767def format_time(seconds: float):768    if seconds < 120:769        return f"`{seconds:.0f}` seconds"770 771    minutes = seconds / 60772    if minutes < 120:773        return f"`{minutes:.0f}` minutes"774 775    hours = minutes / 60776    return f"`{hours:.0f}` hours"777