mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-08 08:11:07 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4354e76fe5 | ||
|
|
4dd77efb75 | ||
|
|
11db6e18ba | ||
|
|
0d6f698e6c | ||
|
|
b8574f78e2 | ||
|
|
e631774a0f | ||
|
|
df735aa748 | ||
|
|
3bdf5b6c65 | ||
|
|
361fa67108 | ||
|
|
c5abcf362a | ||
|
|
7ed2d99a11 | ||
|
|
81307da7c8 | ||
|
|
fb75153b5c | ||
|
|
cddcfcf170 | ||
|
|
398b0473e0 | ||
|
|
c5d260782a | ||
|
|
116ac5a64d | ||
|
|
abf6e2e8d1 | ||
|
|
cb2b3617fa | ||
|
|
6de67c7d0d | ||
|
|
5699d90894 | ||
|
|
2982e8ea9c | ||
|
|
9dd2146342 | ||
|
|
f79184a6ee | ||
|
|
0985e12ad4 | ||
|
|
0fbb7972e1 | ||
|
|
5744ae853e | ||
|
|
bd6e3da4ff | ||
|
|
f2f51d9e8b | ||
|
|
4a1098603d | ||
|
|
5468e35bc1 | ||
|
|
b468267a87 | ||
|
|
48141bbd9e | ||
|
|
b3cdf53d07 | ||
|
|
aa594ddc32 | ||
|
|
5f330e8fb2 | ||
|
|
ff94e128de | ||
|
|
854a016fc5 | ||
|
|
12e8ed7b77 | ||
|
|
13c5f5537e |
No files matched your search
@@ -118,6 +118,8 @@ const agent = new Agent({ instructions: 'You are a helpful AI assistant' });
|
||||
agent.start('Write a movie script about a robot in Mars');
|
||||
```
|
||||
|
||||

|
||||
|
||||
## AI Agents Flow
|
||||
|
||||
```mermaid
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
FROM python:3.11-slim
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN pip install flask praisonai==2.0.63 gunicorn markdown
|
||||
RUN pip install flask praisonai==2.0.76 gunicorn markdown
|
||||
EXPOSE 8080
|
||||
CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]
|
||||
@@ -110,7 +110,7 @@ Loads environment variables from .env file or system and sets them.</p>
|
||||
file.write("FROM python:3.11-slim\n")
|
||||
file.write("WORKDIR /app\n")
|
||||
file.write("COPY . .\n")
|
||||
file.write("RUN pip install flask praisonai==2.0.63 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==2.0.76 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 134 KiB |
@@ -21,6 +21,10 @@ flowchart LR
|
||||
|
||||
PraisonAI CLI provides a simple way to interact with AI agents directly from your terminal. You can run quick commands, specify LLM options, or use YAML configuration files for more complex scenarios.
|
||||
|
||||
<Frame>
|
||||
<img src="../demo/praisonai-cli-demo.gif" alt="PraisonAI CLI Demo" />
|
||||
</Frame>
|
||||
|
||||
## Quick Start
|
||||
|
||||
<Steps>
|
||||
@@ -52,7 +56,7 @@ PraisonAI CLI provides a simple way to interact with AI agents directly from you
|
||||
<Accordion title="With LLM Option" icon="wand-magic-sparkles" defaultOpen>
|
||||
Specify a different LLM model:
|
||||
```bash
|
||||
praisonai "write a movie script in 3 lines" --llm gpt-4-mini
|
||||
praisonai "write a movie script in 3 lines" --llm gpt-4o-mini
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
|
||||
@@ -20,6 +20,20 @@ icon: "dumbbell"
|
||||
export HF_TOKEN=xxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
## Initilise praisonai train
|
||||
|
||||
```bash
|
||||
praisonai train init
|
||||
```
|
||||
|
||||
## Required
|
||||
|
||||
1. Huggingface token
|
||||
2. Base model to train on (e.g. unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit)
|
||||
3. Dataset to train on (e.g. yahma/alpaca-cleaned)
|
||||
4. Huggingface model name to upload to (e.g. mervinpraison/llama3.1-instruct) (Optional)
|
||||
5. Ollama model name to upload to (e.g. mervinpraison/llama3.1-instruct) (Optional)
|
||||
|
||||
## To upload to ollama.com (Linux)
|
||||
|
||||
```bash
|
||||
@@ -108,4 +122,21 @@ quantization_method:
|
||||
|
||||
```bash
|
||||
praisonai train
|
||||
```
|
||||
|
||||
## wandb
|
||||
|
||||
```bash
|
||||
wandb login
|
||||
```
|
||||
|
||||
<Note>
|
||||
Get the key from [here](https://wandb.ai/site/login)
|
||||
</Note>
|
||||
|
||||
```bash
|
||||
export PRAISON_WANDB=True
|
||||
export WANDB_LOG_MODEL=checkpoint
|
||||
export WANDB_PROJECT=praisonai-test
|
||||
export PRAISON_WANDB_RUN_NAME=praisonai-train
|
||||
```
|
||||
+1
-1
@@ -3,7 +3,7 @@ class Praisonai < Formula
|
||||
|
||||
desc "AI tools for various AI applications"
|
||||
homepage "https://github.com/MervinPraison/PraisonAI"
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/2.0.63.tar.gz"
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/2.0.76.tar.gz"
|
||||
sha256 "1828fb9227d10f991522c3f24f061943a254b667196b40b1a3e4a54a8d30ce32" # Replace with actual SHA256 checksum
|
||||
license "MIT"
|
||||
|
||||
|
||||
+29
-7
@@ -25,7 +25,6 @@ CALL_MODULE_AVAILABLE = False
|
||||
CREWAI_AVAILABLE = False
|
||||
AUTOGEN_AVAILABLE = False
|
||||
PRAISONAI_AVAILABLE = False
|
||||
|
||||
try:
|
||||
# Create necessary directories and set CHAINLIT_APP_ROOT
|
||||
if "CHAINLIT_APP_ROOT" not in os.environ:
|
||||
@@ -190,8 +189,7 @@ class PraisonAI:
|
||||
package_root = os.path.dirname(os.path.abspath(__file__))
|
||||
config_yaml_destination = os.path.join(os.getcwd(), 'config.yaml')
|
||||
|
||||
# Create config.yaml only if it doesn't exist or --model or --dataset is provided
|
||||
if not os.path.exists(config_yaml_destination) or args.model or args.dataset:
|
||||
if not os.path.exists(config_yaml_destination):
|
||||
config = generate_config(
|
||||
model_name=args.model,
|
||||
hf_model_name=args.hf,
|
||||
@@ -202,6 +200,20 @@ class PraisonAI:
|
||||
)
|
||||
with open('config.yaml', 'w') as f:
|
||||
yaml.dump(config, f, default_flow_style=False, indent=2)
|
||||
elif args.model or args.hf or args.ollama or (args.dataset and args.dataset != "yahma/alpaca-cleaned"):
|
||||
config = generate_config(
|
||||
model_name=args.model,
|
||||
hf_model_name=args.hf,
|
||||
ollama_model_name=args.ollama,
|
||||
dataset=[{
|
||||
"name": args.dataset
|
||||
}]
|
||||
)
|
||||
with open('config.yaml', 'w') as f:
|
||||
yaml.dump(config, f, default_flow_style=False, indent=2)
|
||||
else:
|
||||
with open(config_yaml_destination, 'r') as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
# Overwrite huggingface_save and ollama_save if --hf or --ollama are provided
|
||||
if args.hf:
|
||||
@@ -228,7 +240,18 @@ class PraisonAI:
|
||||
print("All packages installed.")
|
||||
|
||||
train_args = sys.argv[2:] # Get all arguments after 'train'
|
||||
train_script_path = os.path.join(package_root, 'train.py')
|
||||
|
||||
# Check if this is a vision model - handle all case variations
|
||||
model_name = config.get("model_name", "").lower()
|
||||
is_vision_model = any(x in model_name for x in ["-vl-", "-vl", "vl-", "-vision-", "-vision", "vision-", "visionmodel"])
|
||||
|
||||
# Choose appropriate training script
|
||||
if is_vision_model:
|
||||
train_script_path = os.path.join(package_root, 'train_vision.py')
|
||||
print("Using vision training script for VL model...")
|
||||
else:
|
||||
train_script_path = os.path.join(package_root, 'train.py')
|
||||
print("Using standard training script...")
|
||||
|
||||
# Set environment variables
|
||||
env = os.environ.copy()
|
||||
@@ -393,9 +416,8 @@ class PraisonAI:
|
||||
sys.exit(0)
|
||||
|
||||
elif args.command == 'train':
|
||||
print("[red]ERROR: Train feature is not installed. Install with:[/red]")
|
||||
print("\npip install \"praisonai[train]\"\n")
|
||||
sys.exit(1)
|
||||
package_root = os.path.dirname(os.path.abspath(__file__))
|
||||
config_yaml_destination = os.path.join(os.getcwd(), 'config.yaml')
|
||||
|
||||
elif args.command == 'ui':
|
||||
if not CHAINLIT_AVAILABLE:
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ class CloudDeployer:
|
||||
file.write("FROM python:3.11-slim\n")
|
||||
file.write("WORKDIR /app\n")
|
||||
file.write("COPY . .\n")
|
||||
file.write("RUN pip install flask praisonai==2.0.63 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==2.0.76 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -11,6 +11,38 @@ if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
# Linux
|
||||
MINICONDA_URL="https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh"
|
||||
|
||||
# Install libcurl development package if not present (Debian based)
|
||||
if command -v dpkg &> /dev/null; then
|
||||
if ! dpkg -s libcurl4-openssl-dev &> /dev/null; then
|
||||
echo "libcurl4-openssl-dev is not installed. Installing..."
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libcurl4-openssl-dev
|
||||
else
|
||||
echo "libcurl4-openssl-dev is already installed."
|
||||
fi
|
||||
else
|
||||
echo "Non-Debian based Linux detected. Please ensure libcurl development libraries are installed."
|
||||
fi
|
||||
|
||||
# Check if ollama is installed and executable; if not, install it
|
||||
if ! command -v ollama &> /dev/null; then
|
||||
echo "Ollama is not installed. Installing Ollama..."
|
||||
curl -fsSL https://ollama.com/install.sh | sh
|
||||
|
||||
# Generate SSH key non-interactively only if it doesn't already exist
|
||||
if [ ! -f ~/.ssh/id_ed25519 ]; then
|
||||
echo "Generating SSH key for Ollama..."
|
||||
ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519 -q
|
||||
else
|
||||
echo "SSH key ~/.ssh/id_ed25519 already exists. Skipping generation."
|
||||
fi
|
||||
echo "Copying SSH key to /usr/share/ollama/.ollama..."
|
||||
sudo cp ~/.ssh/id_ed25519 /usr/share/ollama/.ollama
|
||||
else
|
||||
echo "Ollama is already installed."
|
||||
fi
|
||||
|
||||
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then
|
||||
# Windows
|
||||
MINICONDA_URL="https://repo.anaconda.com/miniconda/Miniconda3-latest-Windows-x86_64.exe"
|
||||
@@ -21,12 +53,12 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if conda is already installed
|
||||
# Check if conda is installed
|
||||
if ! command -v conda &> /dev/null; then
|
||||
echo "Conda is not installed. Installing Miniconda..."
|
||||
wget $MINICONDA_URL -O ~/miniconda.sh
|
||||
bash ~/miniconda.sh -b -p $HOME/miniconda
|
||||
source $HOME/miniconda/bin/activate
|
||||
wget "$MINICONDA_URL" -O ~/miniconda.sh
|
||||
bash ~/miniconda.sh -b -p "$HOME/miniconda"
|
||||
source "$HOME/miniconda/bin/activate"
|
||||
conda init
|
||||
else
|
||||
echo "Conda is already installed."
|
||||
@@ -34,39 +66,40 @@ fi
|
||||
|
||||
# Create and activate the Conda environment
|
||||
ENV_NAME="praison_env"
|
||||
if conda info --envs | grep -q $ENV_NAME; then
|
||||
if conda info --envs | grep -q "$ENV_NAME"; then
|
||||
echo "Environment $ENV_NAME already exists. Recreating..."
|
||||
conda env remove -y -n $ENV_NAME # Remove existing environment
|
||||
conda env remove -y -n "$ENV_NAME"
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS (both Intel and M1/M2)
|
||||
conda create --name $ENV_NAME python=3.10 pytorch=2.3.0 -c pytorch -y
|
||||
conda create --name "$ENV_NAME" python=3.10 pytorch=2.3.0 -c pytorch -y
|
||||
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
# Linux
|
||||
conda create --name $ENV_NAME python=3.10 pytorch=2.3.0 cudatoolkit=11.8 -c pytorch -c nvidia -y
|
||||
conda create --name "$ENV_NAME" python=3.10 pytorch=2.3.0 cudatoolkit=11.8 -c pytorch -c nvidia -y
|
||||
fi
|
||||
# conda activate $ENV_NAME
|
||||
else
|
||||
echo "Creating new environment $ENV_NAME..."
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS (both Intel and M1/M2)
|
||||
conda create --name $ENV_NAME python=3.10 pytorch=2.3.0 -c pytorch -y
|
||||
conda create --name "$ENV_NAME" python=3.10 pytorch=2.3.0 -c pytorch -y
|
||||
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
# Linux
|
||||
conda create --name $ENV_NAME python=3.10 pytorch=2.3.0 cudatoolkit=11.8 -c pytorch -c nvidia -y
|
||||
conda create --name "$ENV_NAME" python=3.10 pytorch=2.3.0 cudatoolkit=11.8 -c pytorch -c nvidia -y
|
||||
fi
|
||||
# conda activate $ENV_NAME
|
||||
fi
|
||||
|
||||
# source $HOME/miniconda/bin/activate $ENV_NAME
|
||||
# Activate the environment
|
||||
source "$HOME/miniconda/bin/activate" "$ENV_NAME"
|
||||
|
||||
# Get full path of pip
|
||||
PIP_FULL_PATH=$(conda run -n $ENV_NAME which pip)
|
||||
# Install cmake via conda
|
||||
echo "Installing cmake..."
|
||||
conda install -y cmake
|
||||
|
||||
# Install other packages within the activated environment
|
||||
# Use PIP_FULL_PATH to run pip commands
|
||||
# Get full path of pip within the activated environment
|
||||
PIP_FULL_PATH=$(conda run -n "$ENV_NAME" which pip)
|
||||
|
||||
# Install other packages using pip
|
||||
$PIP_FULL_PATH install --upgrade pip
|
||||
$PIP_FULL_PATH install "xformers==0.0.26.post1"
|
||||
$PIP_FULL_PATH install "unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git@4e570be9ae4ced8cdc64e498125708e34942befc"
|
||||
$PIP_FULL_PATH install "unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git@53a773e4fbc53a1d96c7ba107e5fe75dab07027b"
|
||||
$PIP_FULL_PATH install --no-deps "trl<0.9.0" peft accelerate bitsandbytes
|
||||
$PIP_FULL_PATH install unsloth_zoo
|
||||
$PIP_FULL_PATH install cut_cross_entropy
|
||||
$PIP_FULL_PATH install sentencepiece protobuf datasets huggingface_hub hf_transfer wandb
|
||||
|
||||
echo "Setup completed successfully!"
|
||||
+441
-155
@@ -1,168 +1,287 @@
|
||||
import subprocess
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This script finetunes a model using Unsloth's fast training framework.
|
||||
It supports both ShareGPT and Alpaca‑style datasets by converting raw conversation
|
||||
data into plain-text prompts using a chat template, then pre‑tokenizing the prompts.
|
||||
Extra debug logging is added to help trace the root cause of errors.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import yaml
|
||||
import torch
|
||||
import shutil
|
||||
import subprocess
|
||||
from transformers import TextStreamer
|
||||
from unsloth import FastLanguageModel, is_bfloat16_supported
|
||||
from trl import SFTTrainer
|
||||
from transformers import TrainingArguments
|
||||
from datasets import load_dataset, concatenate_datasets, Dataset
|
||||
from datasets import load_dataset, concatenate_datasets
|
||||
from psutil import virtual_memory
|
||||
from unsloth.chat_templates import standardize_sharegpt, get_chat_template
|
||||
from functools import partial
|
||||
|
||||
class train:
|
||||
#####################################
|
||||
# Step 1: Formatting Raw Conversations
|
||||
#####################################
|
||||
def formatting_prompts_func(examples, tokenizer):
|
||||
"""
|
||||
Converts each example's conversation into a single plain-text prompt.
|
||||
If the example has a "conversations" field, process it as ShareGPT-style.
|
||||
Otherwise, assume Alpaca-style data with "instruction", "input", and "output" fields.
|
||||
"""
|
||||
print("DEBUG: formatting_prompts_func() received batch with keys:", list(examples.keys()))
|
||||
texts = []
|
||||
# Check if the example has a "conversations" field.
|
||||
if "conversations" in examples:
|
||||
for convo in examples["conversations"]:
|
||||
try:
|
||||
formatted = tokenizer.apply_chat_template(
|
||||
convo,
|
||||
tokenize=False, # Return a plain string
|
||||
add_generation_prompt=False
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"ERROR in apply_chat_template (conversations): {e}")
|
||||
formatted = ""
|
||||
# Flatten list if necessary
|
||||
if isinstance(formatted, list):
|
||||
formatted = formatted[0] if len(formatted) == 1 else "\n".join(formatted)
|
||||
texts.append(formatted)
|
||||
else:
|
||||
# Assume Alpaca format: use "instruction", "input", and "output" keys.
|
||||
instructions = examples.get("instruction", [])
|
||||
inputs_list = examples.get("input", [])
|
||||
outputs_list = examples.get("output", [])
|
||||
# If any field is missing, replace with empty string.
|
||||
for ins, inp, out in zip(instructions, inputs_list, outputs_list):
|
||||
# Create a conversation-like structure.
|
||||
convo = [
|
||||
{"role": "user", "content": ins + (f"\nInput: {inp}" if inp.strip() != "" else "")},
|
||||
{"role": "assistant", "content": out}
|
||||
]
|
||||
try:
|
||||
formatted = tokenizer.apply_chat_template(
|
||||
convo,
|
||||
tokenize=False,
|
||||
add_generation_prompt=False
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"ERROR in apply_chat_template (alpaca): {e}")
|
||||
formatted = ""
|
||||
if isinstance(formatted, list):
|
||||
formatted = formatted[0] if len(formatted) == 1 else "\n".join(formatted)
|
||||
texts.append(formatted)
|
||||
if texts:
|
||||
print("DEBUG: Raw texts sample (first 200 chars):", texts[0][:200])
|
||||
return {"text": texts}
|
||||
|
||||
#####################################
|
||||
# Step 2: Tokenizing the Prompts
|
||||
#####################################
|
||||
def tokenize_function(examples, hf_tokenizer, max_length):
|
||||
"""
|
||||
Tokenizes a batch of text prompts with padding and truncation enabled.
|
||||
"""
|
||||
flat_texts = []
|
||||
for t in examples["text"]:
|
||||
if isinstance(t, list):
|
||||
t = t[0] if len(t) == 1 else " ".join(t)
|
||||
flat_texts.append(t)
|
||||
print("DEBUG: Tokenizing a batch of size:", len(flat_texts))
|
||||
tokenized = hf_tokenizer(
|
||||
flat_texts,
|
||||
padding="max_length",
|
||||
truncation=True,
|
||||
max_length=max_length,
|
||||
return_tensors="pt",
|
||||
)
|
||||
tokenized = {key: value.tolist() for key, value in tokenized.items()}
|
||||
sample_key = list(tokenized.keys())[0]
|
||||
print("DEBUG: Tokenized sample (first 10 tokens of", sample_key, "):", tokenized[sample_key][0][:10])
|
||||
return tokenized
|
||||
|
||||
#####################################
|
||||
# Main Training Class
|
||||
#####################################
|
||||
class TrainModel:
|
||||
def __init__(self, config_path="config.yaml"):
|
||||
self.load_config(config_path)
|
||||
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
self.model, self.tokenizer = None, None
|
||||
self.model = None
|
||||
self.hf_tokenizer = None # The underlying HF tokenizer
|
||||
self.chat_tokenizer = None # Chat wrapper for formatting
|
||||
|
||||
def load_config(self, path):
|
||||
with open(path, "r") as file:
|
||||
self.config = yaml.safe_load(file)
|
||||
print("DEBUG: Loaded config:", self.config)
|
||||
|
||||
def print_system_info(self):
|
||||
print(f"PyTorch version: {torch.__version__}")
|
||||
print(f"CUDA version: {torch.version.cuda}")
|
||||
print("DEBUG: PyTorch version:", torch.__version__)
|
||||
print("DEBUG: CUDA version:", torch.version.cuda)
|
||||
if torch.cuda.is_available():
|
||||
device_capability = torch.cuda.get_device_capability()
|
||||
print(f"CUDA Device Capability: {device_capability}")
|
||||
print("DEBUG: CUDA Device Capability:", torch.cuda.get_device_capability())
|
||||
else:
|
||||
print("CUDA is not available")
|
||||
|
||||
python_version = sys.version
|
||||
pip_version = subprocess.check_output(['pip', '--version']).decode().strip()
|
||||
python_path = sys.executable
|
||||
pip_path = subprocess.check_output(['which', 'pip']).decode().strip()
|
||||
print(f"Python Version: {python_version}")
|
||||
print(f"Pip Version: {pip_version}")
|
||||
print(f"Python Path: {python_path}")
|
||||
print(f"Pip Path: {pip_path}")
|
||||
print("DEBUG: CUDA is not available")
|
||||
print("DEBUG: Python Version:", sys.version)
|
||||
print("DEBUG: Python Path:", sys.executable)
|
||||
|
||||
def check_gpu(self):
|
||||
gpu_stats = torch.cuda.get_device_properties(0)
|
||||
print(f"GPU = {gpu_stats.name}. Max memory = {round(gpu_stats.total_memory / 1024 / 1024 / 1024, 3)} GB.")
|
||||
print(f"DEBUG: GPU = {gpu_stats.name}. Max memory = {round(gpu_stats.total_memory/(1024**3),3)} GB.")
|
||||
|
||||
def check_ram(self):
|
||||
ram_gb = virtual_memory().total / 1e9
|
||||
print('Your runtime has {:.1f} gigabytes of available RAM\n'.format(ram_gb))
|
||||
print(f"DEBUG: Your runtime has {ram_gb:.1f} gigabytes of available RAM")
|
||||
if ram_gb < 20:
|
||||
print('Not using a high-RAM runtime')
|
||||
print("DEBUG: Not using a high-RAM runtime")
|
||||
else:
|
||||
print('You are using a high-RAM runtime!')
|
||||
|
||||
# def install_packages(self):
|
||||
# subprocess.run(["pip", "install", "unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git@4e570be9ae4ced8cdc64e498125708e34942befc"])
|
||||
# subprocess.run(["pip", "install", "--no-deps", "trl<0.9.0", "peft==0.12.0", "accelerate==0.33.0", "bitsandbytes==0.43.3"])
|
||||
print("DEBUG: You are using a high-RAM runtime!")
|
||||
|
||||
def prepare_model(self):
|
||||
self.model, self.tokenizer = FastLanguageModel.from_pretrained(
|
||||
print("DEBUG: Preparing model and tokenizer...")
|
||||
self.model, original_tokenizer = FastLanguageModel.from_pretrained(
|
||||
model_name=self.config["model_name"],
|
||||
max_seq_length=self.config["max_seq_length"],
|
||||
dtype=None,
|
||||
load_in_4bit=self.config["load_in_4bit"]
|
||||
load_in_4bit=self.config["load_in_4bit"],
|
||||
)
|
||||
print("DEBUG: Model and original tokenizer loaded.")
|
||||
if original_tokenizer.pad_token is None:
|
||||
original_tokenizer.pad_token = original_tokenizer.eos_token
|
||||
original_tokenizer.model_max_length = self.config["max_seq_length"]
|
||||
self.chat_tokenizer = get_chat_template(original_tokenizer, chat_template="llama-3.1")
|
||||
self.hf_tokenizer = original_tokenizer
|
||||
print("DEBUG: Chat tokenizer created; HF tokenizer saved.")
|
||||
self.model = FastLanguageModel.get_peft_model(
|
||||
self.model,
|
||||
r=self.config["lora_r"],
|
||||
target_modules=self.config["lora_target_modules"],
|
||||
lora_alpha=self.config["lora_alpha"],
|
||||
lora_dropout=self.config["lora_dropout"],
|
||||
bias=self.config["lora_bias"],
|
||||
use_gradient_checkpointing=self.config["use_gradient_checkpointing"],
|
||||
random_state=self.config["random_state"],
|
||||
use_rslora=self.config["use_rslora"],
|
||||
loftq_config=self.config["loftq_config"],
|
||||
r=16,
|
||||
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
|
||||
lora_alpha=16,
|
||||
lora_dropout=0,
|
||||
bias="none",
|
||||
use_gradient_checkpointing="unsloth",
|
||||
random_state=3407,
|
||||
use_rslora=False,
|
||||
loftq_config=None,
|
||||
)
|
||||
print("DEBUG: LoRA adapters added.")
|
||||
|
||||
def process_dataset(self, dataset_info):
|
||||
dataset_name = dataset_info["name"]
|
||||
split_type = dataset_info.get("split_type", "train")
|
||||
processing_func = getattr(self, dataset_info.get("processing_func", "format_prompts"))
|
||||
rename = dataset_info.get("rename", {})
|
||||
filter_data = dataset_info.get("filter_data", False)
|
||||
filter_column_value = dataset_info.get("filter_column_value", "id")
|
||||
filter_value = dataset_info.get("filter_value", "alpaca")
|
||||
num_samples = dataset_info.get("num_samples", 20000)
|
||||
|
||||
print(f"DEBUG: Loading dataset '{dataset_name}' split '{split_type}'...")
|
||||
dataset = load_dataset(dataset_name, split=split_type)
|
||||
|
||||
if rename:
|
||||
dataset = dataset.rename_columns(rename)
|
||||
if filter_data:
|
||||
dataset = dataset.filter(lambda example: filter_value in example[filter_column_value]).shuffle(seed=42).select(range(num_samples))
|
||||
dataset = dataset.map(processing_func, batched=True)
|
||||
print("DEBUG: Dataset columns:", dataset.column_names)
|
||||
if "conversations" in dataset.column_names:
|
||||
print("DEBUG: Standardizing dataset (ShareGPT style)...")
|
||||
dataset = standardize_sharegpt(dataset)
|
||||
else:
|
||||
print("DEBUG: Dataset does not have 'conversations'; assuming Alpaca format.")
|
||||
print("DEBUG: Applying formatting function to dataset...")
|
||||
format_func = partial(formatting_prompts_func, tokenizer=self.chat_tokenizer)
|
||||
dataset = dataset.map(format_func, batched=True, remove_columns=dataset.column_names)
|
||||
sample = dataset[0]
|
||||
print("DEBUG: Sample processed example keys:", list(sample.keys()))
|
||||
if "text" in sample:
|
||||
print("DEBUG: Sample processed 'text' type:", type(sample["text"]))
|
||||
print("DEBUG: Sample processed 'text' content (first 200 chars):", sample["text"][:200])
|
||||
else:
|
||||
print("DEBUG: Processed sample does not contain 'text'.")
|
||||
return dataset
|
||||
|
||||
def format_prompts(self, examples):
|
||||
alpaca_prompt = """Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.
|
||||
|
||||
### Instruction:
|
||||
{}
|
||||
|
||||
### Input:
|
||||
{}
|
||||
|
||||
### Response:
|
||||
{}"""
|
||||
texts = [alpaca_prompt.format(ins, inp, out) + self.tokenizer.eos_token for ins, inp, out in zip(examples["instruction"], examples["input"], examples["output"])]
|
||||
return {"text": texts}
|
||||
def tokenize_dataset(self, dataset):
|
||||
print("DEBUG: Tokenizing the entire dataset...")
|
||||
tokenized_dataset = dataset.map(
|
||||
lambda examples: tokenize_function(examples, self.hf_tokenizer, self.config["max_seq_length"]),
|
||||
batched=True
|
||||
)
|
||||
tokenized_dataset = tokenized_dataset.remove_columns(["text"])
|
||||
print("DEBUG: Tokenized dataset sample keys:", tokenized_dataset[0].keys())
|
||||
return tokenized_dataset
|
||||
|
||||
def load_datasets(self):
|
||||
datasets = []
|
||||
for dataset_info in self.config["dataset"]:
|
||||
print("DEBUG: Processing dataset info:", dataset_info)
|
||||
datasets.append(self.process_dataset(dataset_info))
|
||||
return concatenate_datasets(datasets)
|
||||
combined = concatenate_datasets(datasets)
|
||||
print("DEBUG: Combined dataset has", len(combined), "examples.")
|
||||
return combined
|
||||
|
||||
def train_model(self):
|
||||
dataset = self.load_datasets()
|
||||
print("DEBUG: Starting training...")
|
||||
raw_dataset = self.load_datasets()
|
||||
tokenized_dataset = self.tokenize_dataset(raw_dataset)
|
||||
print("DEBUG: Dataset tokenization complete.")
|
||||
# Build the training arguments parameters dynamically
|
||||
ta_params = {
|
||||
"per_device_train_batch_size": self.config.get("per_device_train_batch_size", 2),
|
||||
"gradient_accumulation_steps": self.config.get("gradient_accumulation_steps", 2),
|
||||
"warmup_steps": self.config.get("warmup_steps", 50),
|
||||
"max_steps": self.config.get("max_steps", 2800),
|
||||
"learning_rate": self.config.get("learning_rate", 2e-4),
|
||||
"fp16": self.config.get("fp16", not is_bfloat16_supported()),
|
||||
"bf16": self.config.get("bf16", is_bfloat16_supported()),
|
||||
"logging_steps": self.config.get("logging_steps", 15),
|
||||
"optim": self.config.get("optim", "adamw_8bit"),
|
||||
"weight_decay": self.config.get("weight_decay", 0.01),
|
||||
"lr_scheduler_type": self.config.get("lr_scheduler_type", "linear"),
|
||||
"seed": self.config.get("seed", 3407),
|
||||
"output_dir": self.config.get("output_dir", "outputs"),
|
||||
"report_to": "none" if not os.getenv("PRAISON_WANDB") else "wandb",
|
||||
"remove_unused_columns": self.config.get("remove_unused_columns", False)
|
||||
}
|
||||
if os.getenv("PRAISON_WANDB"):
|
||||
ta_params["save_steps"] = self.config.get("save_steps", 100)
|
||||
ta_params["run_name"] = os.getenv("PRAISON_WANDB_RUN_NAME", "praisonai-train")
|
||||
|
||||
training_args = TrainingArguments(**ta_params)
|
||||
# Since the dataset is pre-tokenized, we supply a dummy dataset_text_field.
|
||||
trainer = SFTTrainer(
|
||||
model=self.model,
|
||||
tokenizer=self.tokenizer,
|
||||
train_dataset=dataset,
|
||||
dataset_text_field=self.config["dataset_text_field"],
|
||||
tokenizer=self.hf_tokenizer,
|
||||
train_dataset=tokenized_dataset,
|
||||
dataset_text_field="input_ids", # Dummy field since data is numeric
|
||||
max_seq_length=self.config["max_seq_length"],
|
||||
dataset_num_proc=self.config["dataset_num_proc"],
|
||||
packing=self.config["packing"],
|
||||
args=TrainingArguments(
|
||||
per_device_train_batch_size=self.config["per_device_train_batch_size"],
|
||||
gradient_accumulation_steps=self.config["gradient_accumulation_steps"],
|
||||
warmup_steps=self.config["warmup_steps"],
|
||||
num_train_epochs=self.config["num_train_epochs"],
|
||||
max_steps=self.config["max_steps"],
|
||||
learning_rate=self.config["learning_rate"],
|
||||
fp16=not is_bfloat16_supported(),
|
||||
bf16=is_bfloat16_supported(),
|
||||
logging_steps=self.config["logging_steps"],
|
||||
optim=self.config["optim"],
|
||||
weight_decay=self.config["weight_decay"],
|
||||
lr_scheduler_type=self.config["lr_scheduler_type"],
|
||||
seed=self.config["seed"],
|
||||
output_dir=self.config["output_dir"],
|
||||
),
|
||||
dataset_num_proc=1, # Use a single process to avoid pickling issues
|
||||
packing=False,
|
||||
args=training_args,
|
||||
)
|
||||
from unsloth.chat_templates import train_on_responses_only
|
||||
trainer = train_on_responses_only(
|
||||
trainer,
|
||||
instruction_part="<|start_header_id|>user<|end_header_id|>\n\n",
|
||||
response_part="<|start_header_id|>assistant<|end_header_id|>\n\n",
|
||||
)
|
||||
print("DEBUG: Beginning trainer.train() ...")
|
||||
trainer.train()
|
||||
self.model.save_pretrained("lora_model") # Local saving
|
||||
self.tokenizer.save_pretrained("lora_model")
|
||||
print("DEBUG: Training complete. Saving model and tokenizer locally...")
|
||||
self.model.save_pretrained("lora_model")
|
||||
self.hf_tokenizer.save_pretrained("lora_model")
|
||||
print("DEBUG: Saved model and tokenizer to 'lora_model'.")
|
||||
|
||||
def inference(self, instruction, input_text):
|
||||
FastLanguageModel.for_inference(self.model)
|
||||
alpaca_prompt = """Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.
|
||||
messages = [{"role": "user", "content": f"{instruction}\n\nInput: {input_text}"}]
|
||||
inputs = self.hf_tokenizer.apply_chat_template(
|
||||
messages,
|
||||
tokenize=True,
|
||||
add_generation_prompt=True,
|
||||
return_tensors="pt"
|
||||
).to("cuda")
|
||||
outputs = self.model.generate(
|
||||
input_ids=inputs,
|
||||
max_new_tokens=64,
|
||||
use_cache=True,
|
||||
temperature=1.5,
|
||||
min_p=0.1
|
||||
)
|
||||
print("DEBUG: Inference output:", self.hf_tokenizer.batch_decode(outputs))
|
||||
|
||||
### Instruction:
|
||||
{}
|
||||
|
||||
### Input:
|
||||
{}
|
||||
|
||||
### Response:
|
||||
{}"""
|
||||
inputs = self.tokenizer([alpaca_prompt.format(instruction, input_text, "")], return_tensors="pt").to("cuda")
|
||||
outputs = self.model.generate(**inputs, max_new_tokens=64, use_cache=True)
|
||||
print(self.tokenizer.batch_decode(outputs))
|
||||
|
||||
def load_model(self):
|
||||
"""Loads the model and tokenizer using the FastLanguageModel library."""
|
||||
from unsloth import FastLanguageModel
|
||||
model, tokenizer = FastLanguageModel.from_pretrained(
|
||||
model_name=self.config["output_dir"],
|
||||
@@ -177,56 +296,235 @@ class train:
|
||||
shutil.rmtree(self.config["hf_model_name"])
|
||||
self.model.push_to_hub_merged(
|
||||
self.config["hf_model_name"],
|
||||
self.tokenizer,
|
||||
self.hf_tokenizer,
|
||||
save_method="merged_16bit",
|
||||
token=os.getenv('HF_TOKEN')
|
||||
token=os.getenv("HF_TOKEN")
|
||||
)
|
||||
|
||||
def push_model_gguf(self):
|
||||
self.model.push_to_hub_gguf(
|
||||
self.config["hf_model_name"],
|
||||
self.tokenizer,
|
||||
self.hf_tokenizer,
|
||||
quantization_method=self.config["quantization_method"],
|
||||
token=os.getenv('HF_TOKEN')
|
||||
token=os.getenv("HF_TOKEN")
|
||||
)
|
||||
|
||||
|
||||
def save_model_gguf(self):
|
||||
self.model.save_pretrained_gguf(
|
||||
self.config["hf_model_name"],
|
||||
self.tokenizer,
|
||||
self.hf_tokenizer,
|
||||
quantization_method="q4_k_m"
|
||||
)
|
||||
|
||||
def prepare_modelfile_content(self):
|
||||
output_model = self.config["hf_model_name"]
|
||||
gguf_path = f"{output_model}/unsloth.Q4_K_M.gguf"
|
||||
|
||||
# Check if the GGUF file exists. If not, generate it ## TODO Multiple Quantisation other than Q4_K_M.gguf
|
||||
if not os.path.exists(gguf_path):
|
||||
self.model, self.tokenizer = self.load_model()
|
||||
self.save_model_gguf()
|
||||
return f"""FROM {output_model}/unsloth.Q4_K_M.gguf
|
||||
|
||||
TEMPLATE \"\"\"Below are some instructions that describe some tasks. Write responses that appropriately complete each request.{{{{ if .Prompt }}}}
|
||||
|
||||
### Instruction:
|
||||
{{{{ .Prompt }}}}
|
||||
|
||||
{{{{ end }}}}### Response:
|
||||
{{{{ .Response }}}}\"\"\"
|
||||
|
||||
PARAMETER stop ""
|
||||
PARAMETER stop ""
|
||||
PARAMETER stop ""
|
||||
PARAMETER stop ""
|
||||
PARAMETER stop "<|reserved_special_token_"
|
||||
"""
|
||||
model_name = self.config["model_name"].lower()
|
||||
# Mapping from model name keywords to their default TEMPLATE and stop tokens (and optional SYSTEM/num_ctx)
|
||||
mapping = {
|
||||
"llama": {
|
||||
"template": """<|start_header_id|>system<|end_header_id|>
|
||||
Cutting Knowledge Date: December 2023
|
||||
{{ if .System }}{{ .System }}
|
||||
{{- end }}
|
||||
{{- if .Tools }}When you receive a tool call response, use the output to format an answer to the orginal user question.
|
||||
You are a helpful assistant with tool calling capabilities.
|
||||
{{- end }}<|eot_id|>
|
||||
{{- range $i, $_ := .Messages }}
|
||||
{{- $last := eq (len (slice $.Messages $i)) 1 }}
|
||||
{{- if eq .Role "user" }}<|start_header_id|>user<|end_header_id|>
|
||||
{{- if and $.Tools $last }}
|
||||
Given the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt.
|
||||
Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}. Do not use variables.
|
||||
{{ range $.Tools }}
|
||||
{{- . }}
|
||||
{{ end }}
|
||||
{{ .Content }}<|eot_id|>
|
||||
{{- else }}
|
||||
{{ .Content }}<|eot_id|>
|
||||
{{- end }}{{ if $last }}<|start_header_id|>assistant<|end_header_id|>
|
||||
{{ end }}
|
||||
{{- else if eq .Role "assistant" }}<|start_header_id|>assistant<|end_header_id|>
|
||||
{{- if .ToolCalls }}
|
||||
{{ range .ToolCalls }}
|
||||
{"name": "{{ .Function.Name }}", "parameters": {{ .Function.Arguments }}}{{ end }}
|
||||
{{- else }}
|
||||
{{ .Content }}
|
||||
{{- end }}{{ if not $last }}<|eot_id|>{{ end }}
|
||||
{{- else if eq .Role "tool" }}<|start_header_id|>ipython<|end_header_id|>
|
||||
{{ .Content }}<|eot_id|>{{ if $last }}<|start_header_id|>assistant<|end_header_id|>
|
||||
{{ end }}
|
||||
{{- end }}
|
||||
{{- end }}""",
|
||||
"stop_tokens": ["<|start_header_id|>", "<|end_header_id|>", "<|eot_id|>"]
|
||||
},
|
||||
"qwen": {
|
||||
"template": """{{- if .Suffix }}<|fim_prefix|>{{ .Prompt }}<|fim_suffix|>{{ .Suffix }}<|fim_middle|>
|
||||
{{- else if .Messages }}
|
||||
{{- if or .System .Tools }}<|im_start|>system
|
||||
{{- if .System }}
|
||||
{{ .System }}
|
||||
{{- end }}
|
||||
{{- if .Tools }}
|
||||
# Tools
|
||||
You may call one or more functions to assist with the user query.
|
||||
You are provided with function signatures within <tools></tools> XML tags:
|
||||
<tools>
|
||||
{{- range .Tools }}
|
||||
{"type": "function", "function": {{ .Function }}}
|
||||
{{- end }}
|
||||
</tools>
|
||||
For each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:
|
||||
<tool_call>
|
||||
{"name": <function-name>, "arguments": <args-json-object>}
|
||||
</tool_call>
|
||||
{{- end }}<|im_end|>
|
||||
{{ end }}
|
||||
{{- range $i, $_ := .Messages }}
|
||||
{{- $last := eq (len (slice $.Messages $i)) 1 -}}
|
||||
{{- if eq .Role "user" }}<|im_start|>user
|
||||
{{ .Content }}<|im_end|>
|
||||
{{ else if eq .Role "assistant" }}<|im_start|>assistant
|
||||
{{ if .Content }}{{ .Content }}
|
||||
{{- else if .ToolCalls }}<tool_call>
|
||||
{{ range .ToolCalls }}{"name": "{{ .Function.Name }}", "arguments": {{ .Function.Arguments }}}
|
||||
{{ end }}</tool_call>
|
||||
{{- end }}{{ if not $last }}<|im_end|>
|
||||
{{ end }}
|
||||
{{- else if eq .Role "tool" }}<|im_start|>user
|
||||
<tool_response>
|
||||
{{ .Content }}
|
||||
</tool_response><|im_end|>
|
||||
{{ end }}
|
||||
{{- if and (ne .Role "assistant") $last }}<|im_start|>assistant
|
||||
{{ end }}
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
{{- if .System }}<|im_start|>system
|
||||
{{ .System }}<|im_end|>
|
||||
{{ end }}{{ if .Prompt }}<|im_start|>user
|
||||
{{ .Prompt }}<|im_end|>
|
||||
{{ end }}<|im_start|>assistant
|
||||
{{ end }}{{ .Response }}{{ if .Response }}<|im_end|>{{ end }}""",
|
||||
"system": "You are Qwen, created by Alibaba Cloud. You are a helpful assistant.",
|
||||
"num_ctx": 32768,
|
||||
"stop_tokens": ["<|endoftext|>"]
|
||||
},
|
||||
"mistral": {
|
||||
"template": "[INST] {{ if .System }}{{ .System }} {{ end }}{{ .Prompt }} [/INST]",
|
||||
"stop_tokens": ["[INST]", "[/INST]"]
|
||||
},
|
||||
"phi": {
|
||||
"template": """{{- range $i, $_ := .Messages }}
|
||||
{{- $last := eq (len (slice $.Messages $i)) 1 -}}
|
||||
<|im_start|>{{ .Role }}<|im_sep|>
|
||||
{{ .Content }}{{ if not $last }}<|im_end|>
|
||||
{{ end }}
|
||||
{{- if and (ne .Role "assistant") $last }}<|im_end|>
|
||||
<|im_start|>assistant<|im_sep|>
|
||||
{{ end }}
|
||||
{{- end }}""",
|
||||
"stop_tokens": ["<|im_start|>", "<|im_end|>", "<|im_sep|>"]
|
||||
},
|
||||
"deepseek": {
|
||||
"template": """{{- if .System }}{{ .System }}{{ end }}
|
||||
{{- range $i, $_ := .Messages }}
|
||||
{{- $last := eq (len (slice $.Messages $i)) 1}}
|
||||
{{- if eq .Role "user" }}
|
||||
{{ .Content }}
|
||||
{{- else if eq .Role "assistant" }}
|
||||
{{ .Content }}{{- if not $last }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if and $last (ne .Role "assistant") }}
|
||||
{{ end }}
|
||||
{{- end }}""",
|
||||
"stop_tokens": ["", "", "", ""]
|
||||
},
|
||||
"llava": {
|
||||
"template": """{{- if .Suffix }}<|fim_prefix|>{{ .Prompt }}<|fim_suffix|>{{ .Suffix }}<|fim_middle|>
|
||||
{{- else if .Messages }}
|
||||
{{- if or .System .Tools }}<|im_start|>system
|
||||
{{- if .System }}
|
||||
{{ .System }}
|
||||
{{- end }}
|
||||
{{- if .Tools }}
|
||||
# Tools
|
||||
You may call one or more functions to assist with the user query.
|
||||
You are provided with function signatures within <tools></tools> XML tags:
|
||||
<tools>
|
||||
{{- range .Tools }}
|
||||
{"type": "function", "function": {{ .Function }}}
|
||||
{{- end }}
|
||||
</tools>
|
||||
For each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:
|
||||
<tool_call>
|
||||
{"name": <function-name>, "arguments": <args-json-object>}
|
||||
</tool_call>
|
||||
{{- end }}<|im_end|>
|
||||
{{ end }}
|
||||
{{- range $i, $_ := .Messages }}
|
||||
{{- $last := eq (len (slice $.Messages $i)) 1 -}}
|
||||
{{- if eq .Role "user" }}<|im_start|>user
|
||||
{{ .Content }}<|im_end|>
|
||||
{{ else if eq .Role "assistant" }}<|im_start|>assistant
|
||||
{{ if .Content }}{{ .Content }}
|
||||
{{- else if .ToolCalls }}<tool_call>
|
||||
{{ range .ToolCalls }}{"name": "{{ .Function.Name }}", "arguments": {{ .Function.Arguments }}}
|
||||
{{ end }}</tool_call>
|
||||
{{- end }}{{ if not $last }}<|im_end|>
|
||||
{{ end }}
|
||||
{{- else if eq .Role "tool" }}<|im_start|>user
|
||||
<tool_response>
|
||||
{{ .Content }}
|
||||
</tool_response><|im_end|>
|
||||
{{ end }}
|
||||
{{- if and (ne .Role "assistant") $last }}<|im_start|>assistant
|
||||
{{ end }}
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
{{- if .System }}<|im_start|>system
|
||||
{{ .System }}<|im_end|>
|
||||
{{ end }}{{ if .Prompt }}<|im_start|>user
|
||||
{{ .Prompt }}<|im_end|>
|
||||
{{ end }}<|im_start|>assistant
|
||||
{{ end }}{{ .Response }}{{ if .Response }}<|im_end|>{{ end }}""",
|
||||
"stop_tokens": ["</s>", "USER:", "ASSSISTANT:"]
|
||||
}
|
||||
}
|
||||
# Select mapping by checking if any key is in the model_name.
|
||||
chosen = None
|
||||
for key, settings in mapping.items():
|
||||
if key in model_name:
|
||||
chosen = settings
|
||||
break
|
||||
if chosen is None:
|
||||
# Fallback default
|
||||
chosen = {
|
||||
"template": """{{ if .System }}<|start_header_id|>system<|end_header_id|>
|
||||
{{ .System }}<|eot_id|>{{ end }}{{ if .Prompt }}<|start_header_id|>user<|end_header_id|>
|
||||
{{ .Prompt }}<|eot_id|>{{ end }}<|start_header_id|>assistant<|end_header_id|>
|
||||
{{ .Response }}<|eot_id|>""",
|
||||
"stop_tokens": ["<|start_header_id|>", "<|end_header_id|>", "<|eot_id|>"]
|
||||
}
|
||||
# Build the stop parameter lines.
|
||||
stop_params = "\n".join([f"PARAMETER stop {token}" for token in chosen["stop_tokens"]])
|
||||
# Optionally include a SYSTEM line and num_ctx if defined in the mapping.
|
||||
system_line = ""
|
||||
if "system" in chosen:
|
||||
system_line = f"SYSTEM {chosen['system']}\n"
|
||||
num_ctx_line = ""
|
||||
if "num_ctx" in chosen:
|
||||
num_ctx_line = f"PARAMETER num_ctx {chosen['num_ctx']}\n"
|
||||
# Assemble and return the modelfile content.
|
||||
return f"""FROM {output_model}
|
||||
TEMPLATE \"\"\"{chosen['template']}\"\"\"
|
||||
{system_line}{num_ctx_line}{stop_params}
|
||||
"""
|
||||
|
||||
def create_and_push_ollama_model(self):
|
||||
modelfile_content = self.prepare_modelfile_content()
|
||||
with open('Modelfile', 'w') as file:
|
||||
with open("Modelfile", "w") as file:
|
||||
file.write(modelfile_content)
|
||||
|
||||
subprocess.run(["ollama", "serve"])
|
||||
subprocess.run(["ollama", "create", f"{self.config['ollama_model']}:{self.config['model_parameters']}", "-f", "Modelfile"])
|
||||
subprocess.run(["ollama", "push", f"{self.config['ollama_model']}:{self.config['model_parameters']}"])
|
||||
@@ -235,42 +533,30 @@ PARAMETER stop "<|reserved_special_token_"
|
||||
self.print_system_info()
|
||||
self.check_gpu()
|
||||
self.check_ram()
|
||||
# self.install_packages()
|
||||
if self.config.get("train", "true").lower() == "true":
|
||||
self.prepare_model()
|
||||
self.train_model()
|
||||
|
||||
if self.config.get("huggingface_save", "true").lower() == "true":
|
||||
# self.model, self.tokenizer = self.load_model()
|
||||
self.save_model_merged()
|
||||
|
||||
if self.config.get("huggingface_save_gguf", "true").lower() == "true":
|
||||
# self.model, self.tokenizer = self.load_model()
|
||||
self.push_model_gguf()
|
||||
|
||||
# if self.config.get("save_gguf", "true").lower() == "true": ## TODO
|
||||
# self.model, self.tokenizer = self.load_model()
|
||||
# self.save_model_gguf()
|
||||
|
||||
# if self.config.get("save_merged", "true").lower() == "true": ## TODO
|
||||
# self.model, self.tokenizer = self.load_model()
|
||||
# self.save_model_merged()
|
||||
|
||||
if self.config.get("ollama_save", "true").lower() == "true":
|
||||
self.create_and_push_ollama_model()
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description='PraisonAI Training Script')
|
||||
parser.add_argument('command', choices=['train'], help='Command to execute')
|
||||
parser.add_argument('--config', default='config.yaml', help='Path to configuration file')
|
||||
parser = argparse.ArgumentParser(description="PraisonAI Training Script")
|
||||
parser.add_argument("command", choices=["train"], help="Command to execute")
|
||||
parser.add_argument("--config", default="config.yaml", help="Path to configuration file")
|
||||
parser.add_argument("--model", type=str, help="Model name")
|
||||
parser.add_argument("--hf", type=str, help="Hugging Face model name")
|
||||
parser.add_argument("--ollama", type=str, help="Ollama model name")
|
||||
parser.add_argument("--dataset", type=str, help="Dataset name for training")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == 'train':
|
||||
ai = train(config_path=args.config)
|
||||
ai.run()
|
||||
if args.command == "train":
|
||||
trainer_obj = TrainModel(config_path=args.config)
|
||||
trainer_obj.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,274 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This script finetunes a vision language model using Unsloth's fast training framework.
|
||||
It supports vision tasks by converting raw image-caption samples into a conversation format,
|
||||
adding vision-specific LoRA adapters, and training using TRL's SFTTrainer with UnslothVisionDataCollator.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import yaml
|
||||
import torch
|
||||
import shutil
|
||||
import subprocess
|
||||
import gc # For garbage collection
|
||||
|
||||
from datasets import load_dataset, concatenate_datasets, Dataset
|
||||
from unsloth import FastVisionModel, is_bf16_supported
|
||||
from unsloth.trainer import UnslothVisionDataCollator
|
||||
from transformers import TrainingArguments
|
||||
from trl import SFTTrainer
|
||||
from tqdm import tqdm # Add progress bar
|
||||
|
||||
|
||||
class TrainVisionModel:
|
||||
def __init__(self, config_path="config.yaml"):
|
||||
self.load_config(config_path)
|
||||
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
self.model = None
|
||||
self.hf_tokenizer = None # The underlying tokenizer
|
||||
|
||||
def load_config(self, path):
|
||||
with open(path, "r") as file:
|
||||
self.config = yaml.safe_load(file)
|
||||
print("DEBUG: Loaded config:", self.config)
|
||||
|
||||
def print_system_info(self):
|
||||
print("DEBUG: PyTorch version:", torch.__version__)
|
||||
print("DEBUG: CUDA version:", torch.version.cuda)
|
||||
if torch.cuda.is_available():
|
||||
print("DEBUG: CUDA Device Capability:", torch.cuda.get_device_capability())
|
||||
else:
|
||||
print("DEBUG: CUDA is not available")
|
||||
print("DEBUG: Python Version:", sys.version)
|
||||
print("DEBUG: Python Path:", sys.executable)
|
||||
|
||||
def check_gpu(self):
|
||||
gpu_stats = torch.cuda.get_device_properties(0)
|
||||
print(f"DEBUG: GPU = {gpu_stats.name}. Max memory = {round(gpu_stats.total_memory/(1024**3),3)} GB.")
|
||||
|
||||
def check_ram(self):
|
||||
from psutil import virtual_memory
|
||||
ram_gb = virtual_memory().total / 1e9
|
||||
print(f"DEBUG: Your runtime has {ram_gb:.1f} gigabytes of available RAM")
|
||||
if ram_gb < 20:
|
||||
print("DEBUG: Not using a high-RAM runtime")
|
||||
else:
|
||||
print("DEBUG: You are using a high-RAM runtime!")
|
||||
|
||||
def prepare_model(self):
|
||||
print("DEBUG: Preparing vision model and tokenizer...")
|
||||
self.model, original_tokenizer = FastVisionModel.from_pretrained(
|
||||
model_name=self.config["model_name"],
|
||||
load_in_4bit=self.config["load_in_4bit"],
|
||||
use_gradient_checkpointing="unsloth"
|
||||
)
|
||||
print("DEBUG: Vision model and original tokenizer loaded.")
|
||||
|
||||
# Use the full processor that supports image inputs.
|
||||
self.hf_tokenizer = original_tokenizer
|
||||
|
||||
# Set pad token if needed
|
||||
if not hasattr(self.hf_tokenizer, 'pad_token') or self.hf_tokenizer.pad_token is None:
|
||||
if hasattr(self.hf_tokenizer, 'eos_token'):
|
||||
self.hf_tokenizer.pad_token = self.hf_tokenizer.eos_token
|
||||
elif hasattr(self.hf_tokenizer, 'bos_token'):
|
||||
self.hf_tokenizer.pad_token = self.hf_tokenizer.bos_token
|
||||
|
||||
# Set max length
|
||||
if hasattr(self.hf_tokenizer, 'model_max_length'):
|
||||
self.hf_tokenizer.model_max_length = self.config.get("max_seq_length", 2048)
|
||||
|
||||
# Add vision-specific LoRA adapters
|
||||
self.model = FastVisionModel.get_peft_model(
|
||||
self.model,
|
||||
finetune_vision_layers=self.config.get("finetune_vision_layers", False),
|
||||
finetune_language_layers=self.config.get("finetune_language_layers", True),
|
||||
finetune_attention_modules=self.config.get("finetune_attention_modules", True),
|
||||
finetune_mlp_modules=self.config.get("finetune_mlp_modules", True),
|
||||
r=16,
|
||||
lora_alpha=16,
|
||||
lora_dropout=0,
|
||||
bias="none",
|
||||
random_state=3407,
|
||||
use_rslora=False,
|
||||
loftq_config=None
|
||||
)
|
||||
print("DEBUG: Vision LoRA adapters added.")
|
||||
|
||||
def convert_sample(self, sample):
|
||||
|
||||
instruction = self.config.get(
|
||||
"vision_instruction",
|
||||
"You are an expert radiographer. Describe accurately what you see in this image."
|
||||
)
|
||||
conversation = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": instruction},
|
||||
{"type": "image", "image": sample["image"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "text", "text": sample["caption"]}
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
return {"messages": conversation}
|
||||
|
||||
def load_datasets(self):
|
||||
all_converted = []
|
||||
for dataset_info in self.config["dataset"]:
|
||||
print("\nDEBUG: Loading vision dataset:", dataset_info)
|
||||
ds = load_dataset(
|
||||
dataset_info["name"],
|
||||
split=dataset_info.get("split_type", "train")
|
||||
)
|
||||
print("DEBUG: Dataset size:", len(ds))
|
||||
print("DEBUG: First raw sample:", ds[0])
|
||||
print("DEBUG: Dataset features:", ds.features)
|
||||
|
||||
print("\nDEBUG: Converting dataset to vision conversation format...")
|
||||
converted_ds = [self.convert_sample(sample) for sample in ds]
|
||||
|
||||
# Debug first converted sample
|
||||
print("\nDEBUG: First converted sample structure:")
|
||||
first = converted_ds[0]
|
||||
print("DEBUG: Message keys:", first["messages"][0]["content"][1].keys())
|
||||
print("DEBUG: Image type in converted:", type(first["messages"][0]["content"][1].get("image")))
|
||||
|
||||
all_converted.extend(converted_ds)
|
||||
|
||||
print("\nDEBUG: Combined vision dataset has", len(all_converted), "examples.")
|
||||
return all_converted
|
||||
|
||||
def train_model(self):
|
||||
print("DEBUG: Starting vision training...")
|
||||
raw_dataset = self.load_datasets()
|
||||
|
||||
# Build training arguments using TrainingArguments
|
||||
training_args = TrainingArguments(
|
||||
per_device_train_batch_size=self.config.get("per_device_train_batch_size", 1),
|
||||
gradient_accumulation_steps=self.config.get("gradient_accumulation_steps", 4),
|
||||
warmup_steps=self.config.get("warmup_steps", 5),
|
||||
max_steps=self.config.get("max_steps", 30),
|
||||
learning_rate=self.config.get("learning_rate", 2e-4),
|
||||
fp16=self.config.get("fp16", not is_bf16_supported()),
|
||||
bf16=self.config.get("bf16", is_bf16_supported()),
|
||||
logging_steps=self.config.get("logging_steps", 1),
|
||||
optim=self.config.get("optim", "adamw_8bit"),
|
||||
weight_decay=self.config.get("weight_decay", 0.01),
|
||||
lr_scheduler_type=self.config.get("lr_scheduler_type", "linear"),
|
||||
seed=self.config.get("seed", 3407),
|
||||
output_dir=self.config.get("output_dir", "outputs"),
|
||||
report_to="none" if not os.getenv("PRAISON_WANDB") else "wandb",
|
||||
remove_unused_columns=False,
|
||||
# Add memory optimization settings
|
||||
gradient_checkpointing=True,
|
||||
max_grad_norm=1.0,
|
||||
)
|
||||
|
||||
trainer = SFTTrainer(
|
||||
model=self.model,
|
||||
tokenizer=self.hf_tokenizer,
|
||||
data_collator=UnslothVisionDataCollator(self.model, self.hf_tokenizer),
|
||||
train_dataset=raw_dataset,
|
||||
args=training_args,
|
||||
max_seq_length=self.config.get("max_seq_length", 2048),
|
||||
dataset_text_field="", # Required for vision training
|
||||
dataset_kwargs={"skip_prepare_dataset": True}, # Required for vision training
|
||||
packing=False # Explicitly set packing to False
|
||||
)
|
||||
print("DEBUG: Beginning vision trainer.train() ...")
|
||||
trainer.train()
|
||||
print("DEBUG: Vision training complete. Saving model and tokenizer locally...")
|
||||
self.model.save_pretrained("lora_vision_model")
|
||||
self.hf_tokenizer.save_pretrained("lora_vision_model")
|
||||
print("DEBUG: Saved vision model and tokenizer to 'lora_vision_model'.")
|
||||
|
||||
def vision_inference(self, instruction, image):
|
||||
FastVisionModel.for_inference(self.model)
|
||||
messages = [
|
||||
{"role": "user", "content": [
|
||||
{"type": "image"},
|
||||
{"type": "text", "text": instruction}
|
||||
]}
|
||||
]
|
||||
input_text = self.hf_tokenizer.apply_chat_template(messages, add_generation_prompt=True)
|
||||
inputs = self.hf_tokenizer(
|
||||
image,
|
||||
input_text,
|
||||
add_special_tokens=False,
|
||||
return_tensors="pt"
|
||||
).to("cuda")
|
||||
outputs = self.model.generate(
|
||||
**inputs,
|
||||
max_new_tokens=128,
|
||||
use_cache=True,
|
||||
temperature=1.5,
|
||||
min_p=0.1
|
||||
)
|
||||
print("DEBUG: Vision inference output:", self.hf_tokenizer.batch_decode(outputs))
|
||||
|
||||
def save_model_merged(self):
|
||||
if os.path.exists(self.config["hf_model_name"]):
|
||||
shutil.rmtree(self.config["hf_model_name"])
|
||||
self.model.push_to_hub_merged(
|
||||
self.config["hf_model_name"],
|
||||
self.hf_tokenizer,
|
||||
save_method="merged_16bit",
|
||||
token=os.getenv("HF_TOKEN")
|
||||
)
|
||||
|
||||
def push_model_gguf(self):
|
||||
self.model.push_to_hub_gguf(
|
||||
self.config["hf_model_name"],
|
||||
self.hf_tokenizer,
|
||||
quantization_method=self.config.get("quantization_method", "q4_k_m"),
|
||||
token=os.getenv("HF_TOKEN")
|
||||
)
|
||||
|
||||
def save_model_gguf(self):
|
||||
self.model.save_pretrained_gguf(
|
||||
self.config["hf_model_name"],
|
||||
self.hf_tokenizer,
|
||||
quantization_method="q4_k_m"
|
||||
)
|
||||
|
||||
def run(self):
|
||||
self.print_system_info()
|
||||
self.check_gpu()
|
||||
self.check_ram()
|
||||
if self.config.get("train", "true").lower() == "true":
|
||||
self.prepare_model()
|
||||
self.train_model()
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description="PraisonAI Vision Training Script")
|
||||
parser.add_argument("command", choices=["train", "inference"], help="Command to execute")
|
||||
parser.add_argument("--config", default="config.yaml", help="Path to configuration file")
|
||||
args = parser.parse_args()
|
||||
|
||||
trainer_obj = TrainVisionModel(config_path=args.config)
|
||||
if args.command == "train":
|
||||
trainer_obj.run()
|
||||
elif args.command == "inference":
|
||||
# For inference, we load a sample image from the first dataset
|
||||
instr = trainer_obj.config.get("vision_instruction", "You are an expert radiographer. Describe accurately what you see in this image.")
|
||||
ds_info = trainer_obj.config["dataset"][0]
|
||||
ds = load_dataset(ds_info["name"], split=ds_info.get("split_type", "train"))
|
||||
sample_image = ds[0]["image"]
|
||||
if trainer_obj.model is None or trainer_obj.hf_tokenizer is None:
|
||||
trainer_obj.prepare_model()
|
||||
trainer_obj.vision_inference(instr, sample_image)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "PraisonAI"
|
||||
version = "2.0.63"
|
||||
version = "2.0.76"
|
||||
description = "PraisonAI is an AI Agents Framework with Self Reflection. PraisonAI application combines PraisonAI Agents, AutoGen, and CrewAI into a low-code solution for building and managing multi-agent LLM systems, focusing on simplicity, customisation, and efficient human-agent collaboration."
|
||||
readme = "README.md"
|
||||
license = ""
|
||||
@@ -84,7 +84,7 @@ autogen = ["pyautogen>=0.2.19", "praisonai-tools>=0.0.7", "crewai"]
|
||||
|
||||
[tool.poetry]
|
||||
name = "PraisonAI"
|
||||
version = "2.0.63"
|
||||
version = "2.0.76"
|
||||
description = "PraisonAI is an AI Agents Framework with Self Reflection. PraisonAI application combines PraisonAI Agents, AutoGen, and CrewAI into a low-code solution for building and managing multi-agent LLM systems, focusing on simplicity, customisation, and efficient human–agent collaboration."
|
||||
authors = ["Mervin Praison"]
|
||||
license = ""
|
||||
|
||||
Reference in new issue
Block a user