| import json |
| from pprint import pprint |
| from typing import List, Dict, Optional, Generator |
| |
| def load_config(file_path: str = 'config.json') -> Dict: |
| with open(file_path, 'r') as file: |
| config = json.load(file) |
| pprint(config) |
| return config |
| config = load_config('config.json') |
|
|
| |
| import os |
| import re |
| import shutil |
| import functools |
| from datetime import datetime |
|
|
| import torch |
| import numpy as np |
| import gradio as gr |
| import huggingface_hub |
| from datasets import Dataset |
| from transformers import AutoTokenizer, AutoModelForCausalLM, GenerationConfig, StoppingCriteria, StoppingCriteriaList |
| from peft import LoraConfig, get_peft_model, PeftModel, PeftConfig, get_peft_model_state_dict |
| from callbacks import Iteratorize |
|
|
| base_dir = os.path.dirname(os.path.abspath('__file__')) |
| print(f"Base directory: {base_dir}") |
|
|
| |
| HF_TOKEN = os.environ.get("HF_TOKEN") |
| huggingface_hub.login(HF_TOKEN) |
| api = huggingface_hub.HfApi() |
|
|
| |
| def extract_base_model(config: Dict) -> str: |
| model_variant = config['hf_load_model_name'][-2:].lower() |
| if model_variant == "3b": |
| return "togethercomputer/RedPajama-INCITE-Chat-3B-v1" |
| else: |
| return "togethercomputer/RedPajama-INCITE-7B-Chat" |
| base_model = extract_base_model(config) |
| print(f"Using base model: {base_model}") |
|
|
| |
| tokenizer = AutoTokenizer.from_pretrained(base_model) |
|
|
|
|
| |
| additional_instruction = "The details below description the character, utilizing any relevant detail in your response.\n" |
| if config['hf_load_model_name']: |
| if not os.path.exists("lora_weights"): |
| os.mkdir("lora_weights") |
| |
| huggingface_hub.hf_hub_download(repo_id=f"sortxyz/{config['hf_load_model_repo']}", |
| filename=f"{config['hf_load_model_name']}/adapter_model.bin", |
| repo_type="model", |
| local_dir="lora_weights") |
| |
| huggingface_hub.hf_hub_download(repo_id=f"sortxyz/{config['hf_load_model_repo']}", |
| filename=f"{config['hf_load_model_name']}/adapter_config.json", |
| repo_type="model", |
| local_dir="lora_weights") |
| huggingface_hub.hf_hub_download(repo_id=f"sortxyz/{config['hf_load_model_repo']}", |
| filename=f"{config['hf_load_model_name']}/additional_instruction.json", |
| repo_type="model", |
| local_dir="lora_weights") |
| |
| with open(f"{base_dir}/lora_weights/{config['hf_load_model_name']}/additional_instruction.json", 'r') as file: |
| additional_instruction = json.load(file)['additional_instruction'] |
| os.remove(f"{base_dir}/lora_weights/{config['hf_load_model_name']}/additional_instruction.json") |
|
|
| |
| instruction_empty_prompt = """<human>:{}\n""" + additional_instruction + """{}\n<bot>:""" |
| response_empty_prompt = instruction_empty_prompt + """{}\n<human>:""" |
| print(f"{'#'*25} Empty response prompt {'#'*25}\n" + response_empty_prompt + f"\n{'#'*23} Empty response prompt END {'#'*23}") |
|
|
| def reformat_row(row): |
| """ |
| Converts 'attributes' list of a given row into a dictionary, reducing the string length by approximately 2x. |
| |
| Parameters: |
| row (dict): Contains 'id' (int) and 'attributes' (list of dictionaries). |
| {'id': 979, 'attributes': [{'value': 'Chillbucks Apron', 'trait_type': 'Accessories'}, {'value': 'Black Shirt', 'trait_type': 'Apparel'}, {'value': 'Blue', 'trait_type': 'Background'}, {'value': 'Uhhh', 'trait_type': 'Expression'}, {'value': 'Short Curly Black', 'trait_type': 'Hair'}, {'value': 'Purple', 'trait_type': 'Skin'}]} |
| |
| Returns: |
| dict: Original 'id' with 'trait_type' and 'value' pairs from 'attributes' as new keys and values. |
| {'id': 979, 'Accessories': 'Chillbucks Apron', 'Apparel': 'Black Shirt', 'Background': 'Blue', 'Expression': 'Uhhh', 'Hair': 'Short Curly Black', 'Skin': 'Purple'} |
| """ |
| return {**{'id': row['id']}, **{pair['trait_type'] : pair['value'] for pair in row['attributes']}} |
|
|
| |
| if not os.path.exists("downloaded_data"): |
| os.mkdir("downloaded_data") |
| huggingface_hub.hf_hub_download(repo_id=f"sortxyz/{config['hf_complete_dataset_repo']}", |
| filename=config['hf_complete_dataset_name'], |
| repo_type="dataset", |
| local_dir="downloaded_data") |
| shutil.move(f"downloaded_data/{config['hf_complete_dataset_name']}", 'complete_dataset.jsonl') |
|
|
| |
| |
| with open('complete_dataset.jsonl', 'r') as fp: |
| complete_dataset_raw = [json.loads(x) for x in fp.readlines()] |
| complete_dataset = [reformat_row(data_dict) for data_dict in complete_dataset_raw] |
| data_indexed_by_id = {data_dict['id'] : data_dict for data_dict in complete_dataset} |
| print(f"length of complete_dataset: {len(complete_dataset)}") |
|
|
| def find_relevant_rows(instruction): |
| """Used to append relevant information to character(s) referenced in prompt |
| Given a string it extracts the corresponding rows |
| e.g. "Describe ID 972 and 979" returns |
| {'id': 972, 'Accessories': 'Smoke Frame Glasses', 'Apparel': 'Blue Button Up', 'Background': 'Blue', 'Expression': 'Chill Smile', 'Facial Features': 'Stuble Goatee', 'Hair': 'Short Blond', 'Skin': 'Orange'} |
| {'id': 979, 'Accessories': 'Chillbucks Apron', 'Apparel': 'Black Shirt', 'Background': 'Blue', 'Expression': 'Uhhh', 'Hair': 'Short Curly Black', 'Skin': 'Purple'} |
| """ |
| ids_found = [int(n) for n in re.findall(r'\d+', instruction)] |
| rows_returned = [] |
| for id in ids_found: |
| if id not in data_indexed_by_id.keys(): |
| rows_returned.append(f"ID {id} is not contained in dataset") |
| else: |
| rows_returned.append(str(data_indexed_by_id[id])) |
| return "\n".join(rows_returned) |
|
|
| |
| persistent_dataset_repo_url = "https://huggingface.co/datasets/sortxyz/persistent-space-dataset" |
| persistent_data_filename = f"{base_dir}/data/{config['persistent_data_filename']}" |
|
|
| repo = huggingface_hub.Repository(local_dir="data", clone_from=persistent_dataset_repo_url, use_auth_token=HF_TOKEN) |
| if not os.path.exists(persistent_data_filename): |
| persistent_data = {"data": []} |
| else: |
| with open(persistent_data_filename, 'r') as f: |
| persistent_data = json.load(f) |
|
|
| def store_persistent_information(user_prompt: str, model_prompt: str, generate_output: str, print_commit_url=False): |
| """Appends message to persistent data .json file and uploads it to hf dataset repo on every call. |
| Note: repo is only retrieved at the start of the runtime, so will not work for spaces in parallele""" |
| persistent_data['data'].append({"user_prompt": user_prompt, "model_prompt": model_prompt, "generate_output": generate_output, "time": datetime.now().strftime('%Y-%m-%d %H:%M:%S')}) |
| with open(persistent_data_filename, 'w') as f: |
| json.dump(persistent_data, f, indent=4) |
| commit_url = repo.push_to_hub() |
| if print_commit_url: |
| print(commit_url) |
|
|
| class StopWordsCriteria(StoppingCriteria): |
| """ |
| Class for stopping output generation when the "<human>" token is encountered. |
| Prevents the display of any remaining unwanted tokens during streaming. |
| Buffering would be faster, but it is incompatible with multiple beams, |
| so we must decode after every new token. |
| """ |
|
|
| def __init__(self, tokenizer, stop_words: list = ["<human>"], prompt_length: int = -1, stream_callback: bool = False): |
| self._tokenizer = tokenizer |
| self._stop_words = stop_words |
| self.prompt_length = prompt_length |
| self._stream_callback = stream_callback |
|
|
| def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor, **kwargs) -> bool: |
| if self.prompt_length == -1: |
| self.prompt_length = len(input_ids[0]) - 1 |
|
|
| |
| decoded_text = self._tokenizer.decode(input_ids[0][self.prompt_length:]) |
| for stop_word in self._stop_words: |
| if stop_word in decoded_text: |
| return True |
|
|
| if self._stream_callback: |
| |
| last_characters_to_miss = 0 |
| for stop_word in self._stop_words: |
| for i in range(1, len(stop_word)): |
| if decoded_text.endswith(stop_word[0:i]): |
| last_characters_to_miss = max(i, last_characters_to_miss) |
|
|
| |
| self._stream_callback(decoded_text) if last_characters_to_miss == 0 else self._stream_callback(decoded_text[:-last_characters_to_miss]) |
| return False |
|
|
| def evaluate( |
| model, |
| instruction, |
| temperature=0.1, |
| top_p=0.75, |
| top_k=40, |
| num_beams=4, |
| max_new_tokens=256, |
| stream_output=False, |
| store_outputs=True, |
| print_progress=True, |
| **kwargs, |
| ): |
| """Evaluate the model using the provided instruction and generation parameters. |
| Yields: |
| The generated output as a response to the provided instruction (Directly or via streaming). |
| """ |
| prompt = instruction_empty_prompt.format(instruction, find_relevant_rows(instruction)) |
| if print_progress: |
| print("\n" + "#"*50 + "\n" + prompt) |
|
|
| |
| inputs = tokenizer(prompt, return_tensors="pt") |
| input_ids = inputs["input_ids"].to("cuda:0") |
|
|
| |
| generate_params = { |
| "input_ids": input_ids, |
| "generation_config": GenerationConfig( |
| temperature=temperature, |
| top_p=top_p, |
| top_k=top_k, |
| num_beams=num_beams, |
| **kwargs), |
| "return_dict_in_generate": True, |
| "output_scores": True, |
| "max_new_tokens": max_new_tokens, |
| } |
|
|
| if stream_output: |
| |
| |
| |
|
|
| def generate_with_callback(callback=None, **kwargs): |
| kwargs.setdefault( |
| "stopping_criteria", StoppingCriteriaList( |
| [StopWordsCriteria(tokenizer, |
| stream_callback=callback)]) |
| ) |
| with torch.no_grad(): |
| model.generate(**kwargs) |
|
|
| def generate_with_streaming(**kwargs): |
| return Iteratorize(generate_with_callback, kwargs, callback=None) |
|
|
| with generate_with_streaming(**generate_params) as generator: |
| for output in generator: |
| yield output |
| else: |
| |
| with torch.no_grad(): |
| generation_output = model.generate( |
| **generate_params, |
| stopping_criteria=StoppingCriteriaList([StopWordsCriteria(tokenizer)]) |
| ) |
| unformatted_output = tokenizer.decode(generation_output.sequences[0]) |
| |
| output = unformatted_output[len(prompt):-len('<human>:<|endoftext|>')].strip(' \n') |
| if print_progress: |
| print(output) |
| if store_outputs: |
| store_persistent_information(instruction, prompt, output, print_commit_url=True) |
| yield output |
|
|
| def create_gradio_interface( |
| model: PeftModel, |
| config: Dict, |
| ) -> gr.Interface: |
| """Creates and launches a Gradio Interface using given model and configuration parameters. |
| |
| Args: |
| model (PeftModel): The model to be evaluated. |
| config (dict): Configuration parameters for the Gradio Interface. |
| |
| Returns: |
| gr.Interface: Gradio interface object.""" |
|
|
| gradio_inputs = [ |
| gr.components.Textbox(lines=2, label="Instruction", placeholder=config['gradio_placeholder'], value=config['gradio_value']), |
| gr.components.Slider(minimum=0, maximum=1, value=0.1, label="Temperature"), |
| gr.components.Slider(minimum=0, maximum=1, value=0.75, label="Top p"), |
| gr.components.Slider(minimum=0, maximum=100, step=1, value=40, label="Top k"), |
| gr.components.Slider(minimum=1, maximum=4, step=1, value=4, label="Beams"), |
| gr.components.Slider(minimum=1, maximum=2000, step=1, value=256, label="Max tokens"), |
| gr.components.Checkbox(label="Stream output", value=True), |
| gr.components.Checkbox(label="Store user prompts (to improve future model versions)", value=False), |
| ] |
|
|
| interface = gr.Interface( |
| fn=functools.partial(evaluate, model), |
| inputs=gradio_inputs, |
| outputs=[gr.inputs.Textbox(lines=5, label="Generation")], |
| title=config['gradio_title'], |
| description=config['gradio_description'], |
| examples=config['gradio_examples'], |
| cache_examples=False |
| ) |
|
|
| interface.queue(concurrency_count=config['concurrency_count']) |
| return interface |
|
|
| |
| model = AutoModelForCausalLM.from_pretrained( |
| base_model, |
| load_in_8bit=True, |
| device_map="auto") |
|
|
| |
| if config['hf_load_model_name']: |
| model = PeftModel.from_pretrained( |
| model, |
| f"lora_weights/{config['hf_load_model_name']}") |
|
|
| |
| |
| |
|
|
| |
| create_gradio_interface(model, config).launch(debug=True) |