diff --git a/cli/alora/train.py b/cli/alora/train.py index a04f3b5b..5bd0ea43 100644 --- a/cli/alora/train.py +++ b/cli/alora/train.py @@ -1,14 +1,34 @@ import json import os +import sys +import warnings +import torch import typer -from alora.config import aLoraConfig -from alora.peft_model_alora import aLoRAPeftModelForCausalLM from datasets import Dataset -from peft import LoraConfig, PeftModelForCausalLM +from peft import LoraConfig, get_peft_model from transformers import AutoModelForCausalLM, AutoTokenizer, TrainerCallback from trl import DataCollatorForCompletionOnlyLM, SFTConfig, SFTTrainer +# Handle MPS with old PyTorch versions on macOS only +# Accelerate's GradScaler requires PyTorch >= 2.8.0 for MPS +if sys.platform == "darwin" and hasattr(torch.backends, "mps"): + if torch.backends.mps.is_available(): + pytorch_version = tuple(int(x) for x in torch.__version__.split(".")[:2]) + if pytorch_version < (2, 8): + # Disable MPS detection to force CPU usage on macOS + # This must be done before any models or tensors are initialized + torch.backends.mps.is_available = lambda: False # type: ignore[assignment] + torch.backends.mps.is_built = lambda: False # type: ignore[assignment] + os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "0" + warnings.warn( + "MPS is available but PyTorch < 2.8.0. Disabling MPS to avoid " + "gradient scaling issues. Training will run on CPU. " + "To use MPS, upgrade to PyTorch >= 2.8.0.", + UserWarning, + stacklevel=2, + ) + def load_dataset_from_json(json_path, tokenizer, invocation_prompt): data = [] @@ -90,8 +110,12 @@ def train_model( train_dataset = dataset.select(range(split_idx)) val_dataset = dataset.select(range(split_idx, len(dataset))) + # Use device_map="auto" only when CUDA is available + # In CPU-only environments (like CI), device_map="auto" creates meta tensors + # which cause "Cannot copy out of meta tensor" errors + device_map = "auto" if torch.cuda.is_available() else None model_base = AutoModelForCausalLM.from_pretrained( - base_model, device_map="auto", use_cache=False + base_model, device_map=device_map, use_cache=False ) collator = DataCollatorForCompletionOnlyLM(invocation_prompt, tokenizer=tokenizer) @@ -100,21 +124,21 @@ def train_model( os.makedirs(output_dir, exist_ok=True) if adapter == "alora": - peft_config = aLoraConfig( - invocation_string=invocation_prompt, + # Tokenize the invocation string for PEFT 0.18.0 native aLoRA + invocation_token_ids = tokenizer.encode( + invocation_prompt, add_special_tokens=False + ) + + peft_config = LoraConfig( r=32, lora_alpha=32, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", target_modules=["q_proj", "k_proj", "v_proj"], + alora_invocation_tokens=invocation_token_ids, # Enable aLoRA ) - response_token_ids = tokenizer( - invocation_prompt, return_tensors="pt", add_special_tokens=False - )["input_ids"] - model = aLoRAPeftModelForCausalLM( - model_base, peft_config, response_token_ids=response_token_ids - ) + model = get_peft_model(model_base, peft_config) sft_args = SFTConfig( output_dir=output_dir, @@ -148,7 +172,7 @@ def train_model( task_type="CAUSAL_LM", target_modules=["q_proj", "k_proj", "v_proj"], ) - model = PeftModelForCausalLM(model_base, peft_config) + model = get_peft_model(model_base, peft_config) sft_args = SFTConfig( output_dir=output_dir, diff --git a/docs/alora.md b/docs/alora.md index 75d02037..d17f721c 100644 --- a/docs/alora.md +++ b/docs/alora.md @@ -1,6 +1,6 @@ # Mellea CLI — Train & Upload LoRA/aLoRA Adapters -Mellea provides a command-line interface for training and uploading [LoRA](https://arxiv.org/abs/2106.09685) or [aLoRA](https://github.com/IBM/alora) adapters for causal language models. This tool is useful for adapting base models like IBM Granite to custom tasks using prompt-based classification. The major goal is to help customer train a requirement validator. +Mellea provides a command-line interface for training and uploading [LoRA](https://arxiv.org/abs/2106.09685) or [aLoRA](https://huggingface.co/docs/peft/main/en/package_reference/lora#alora) adapters for causal language models. This tool is useful for adapting base models like IBM Granite to custom tasks using prompt-based classification. The major goal is to help customer train a requirement validator. --- @@ -82,13 +82,12 @@ This will: ## 🛠 Requirements - Python 3.8+ -- Install the following dependencies manually or via `pip install mellea`: +- Install the following dependencies manually or via `pip install mellea[hf]`: - `transformers` - `trl` - - `peft` + - `peft>=0.18.1` (native aLoRA support) - `datasets` - `huggingface_hub` - - `alora` --- diff --git a/pyproject.toml b/pyproject.toml index 8f7223aa..bba442cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,11 +69,10 @@ m = "cli.m:cli" hf = [ "accelerate>=1.9.0", - "alora==0.2.0", "datasets>=4.0.0", "outlines-core==0.1.26", "outlines", # intentionally un-versioned, expecting a minor update. coutlines-core version should be enough to specify it - "peft>=0.18.0", # aLoRA support was added in Peft 0.18.0 + "peft>=0.18.1", # Native aLoRA support added in PEFT 0.18.0 "transformers>=4.53.2,<5", "trl==0.19.1", "granite-common[transformers]", diff --git a/test/cli/test_alora_train.py b/test/cli/test_alora_train.py new file mode 100644 index 00000000..75370cad --- /dev/null +++ b/test/cli/test_alora_train.py @@ -0,0 +1,221 @@ +"""Unit tests for aLoRA/LoRA training configuration.""" + +from unittest.mock import MagicMock, Mock, patch + +import pytest +from peft import LoraConfig + + +@pytest.mark.huggingface +def test_alora_config_creation(): + """Test that aLoRA config is created correctly with PEFT 0.18+.""" + from cli.alora.train import train_model + + # Mock all the heavy dependencies + with ( + patch("cli.alora.train.AutoTokenizer") as mock_tokenizer_class, + patch("cli.alora.train.AutoModelForCausalLM") as mock_model_class, + patch("cli.alora.train.Dataset"), + patch("cli.alora.train.SafeSaveTrainer") as mock_trainer, + patch("cli.alora.train.get_peft_model") as mock_get_peft_model, + patch("cli.alora.train.load_dataset_from_json") as mock_load_dataset, + patch("cli.alora.train.DataCollatorForCompletionOnlyLM"), + ): + # Setup mocks + mock_tokenizer = Mock() + mock_tokenizer.encode.return_value = [123, 456, 789] # Mock token IDs + mock_tokenizer.eos_token = "" + mock_tokenizer_class.from_pretrained.return_value = mock_tokenizer + + mock_model = Mock() + mock_model_class.from_pretrained.return_value = mock_model + + mock_peft_model = Mock() + mock_get_peft_model.return_value = mock_peft_model + + # Mock dataset + mock_ds = MagicMock() + mock_ds.shuffle.return_value = mock_ds + mock_ds.select.return_value = mock_ds + mock_ds.__len__ = Mock(return_value=10) + mock_load_dataset.return_value = mock_ds + + # Mock trainer + mock_trainer_instance = Mock() + mock_trainer.return_value = mock_trainer_instance + + # Call train_model with aLoRA adapter + train_model( + dataset_path="test.jsonl", + base_model="test-model", + output_file="./test_output/adapter", + adapter="alora", + epochs=1, + ) + + # Verify get_peft_model was called + assert mock_get_peft_model.called, "get_peft_model should be called" + + # Get the LoraConfig that was passed to get_peft_model + call_args = mock_get_peft_model.call_args + assert call_args is not None, ( + "get_peft_model should have been called with arguments" + ) + + peft_config = call_args[0][1] # Second argument is the config + + # Verify it's a LoraConfig + assert isinstance(peft_config, LoraConfig), "Should use LoraConfig" + + # Verify aLoRA-specific parameter is set + assert hasattr(peft_config, "alora_invocation_tokens"), ( + "Config should have alora_invocation_tokens attribute" + ) + assert peft_config.alora_invocation_tokens == [123, 456, 789], ( + "alora_invocation_tokens should match tokenized invocation prompt" + ) + + # Verify other LoRA parameters + assert peft_config.r == 32, "Rank should be 32 for aLoRA" + assert peft_config.lora_alpha == 32, "Alpha should be 32" + assert peft_config.task_type == "CAUSAL_LM", "Task type should be CAUSAL_LM" + + +@pytest.mark.huggingface +def test_lora_config_creation(): + """Test that standard LoRA config is created correctly.""" + from cli.alora.train import train_model + + # Mock all the heavy dependencies + with ( + patch("cli.alora.train.AutoTokenizer") as mock_tokenizer_class, + patch("cli.alora.train.AutoModelForCausalLM") as mock_model_class, + patch("cli.alora.train.Dataset"), + patch("cli.alora.train.SafeSaveTrainer") as mock_trainer, + patch("cli.alora.train.get_peft_model") as mock_get_peft_model, + patch("cli.alora.train.load_dataset_from_json") as mock_load_dataset, + patch("cli.alora.train.DataCollatorForCompletionOnlyLM"), + ): + # Setup mocks + mock_tokenizer = Mock() + mock_tokenizer.eos_token = "" + mock_tokenizer_class.from_pretrained.return_value = mock_tokenizer + + mock_model = Mock() + mock_model_class.from_pretrained.return_value = mock_model + + mock_peft_model = Mock() + mock_get_peft_model.return_value = mock_peft_model + + # Mock dataset + mock_ds = MagicMock() + mock_ds.shuffle.return_value = mock_ds + mock_ds.select.return_value = mock_ds + mock_ds.__len__ = Mock(return_value=10) + mock_load_dataset.return_value = mock_ds + + # Mock trainer + mock_trainer_instance = Mock() + mock_trainer.return_value = mock_trainer_instance + + # Call train_model with standard LoRA adapter + train_model( + dataset_path="test.jsonl", + base_model="test-model", + output_file="./test_output/adapter", + adapter="lora", # Standard LoRA, not aLoRA + epochs=1, + ) + + # Verify get_peft_model was called + assert mock_get_peft_model.called, "get_peft_model should be called" + + # Get the LoraConfig that was passed to get_peft_model + call_args = mock_get_peft_model.call_args + assert call_args is not None, ( + "get_peft_model should have been called with arguments" + ) + + peft_config = call_args[0][1] # Second argument is the config + + # Verify it's a LoraConfig + assert isinstance(peft_config, LoraConfig), "Should use LoraConfig" + + # Verify aLoRA-specific parameter is NOT set for standard LoRA + assert ( + not hasattr(peft_config, "alora_invocation_tokens") + or peft_config.alora_invocation_tokens is None + ), "Standard LoRA should not have alora_invocation_tokens" + + # Verify other LoRA parameters + assert peft_config.r == 6, "Rank should be 6 for standard LoRA" + assert peft_config.lora_alpha == 32, "Alpha should be 32" + assert peft_config.task_type == "CAUSAL_LM", "Task type should be CAUSAL_LM" + + +@pytest.mark.huggingface +def test_invocation_prompt_tokenization(): + """Test that invocation prompt is correctly tokenized for aLoRA.""" + from cli.alora.train import train_model + + with ( + patch("cli.alora.train.AutoTokenizer") as mock_tokenizer_class, + patch("cli.alora.train.AutoModelForCausalLM") as mock_model_class, + patch("cli.alora.train.get_peft_model") as mock_get_peft_model, + patch("cli.alora.train.load_dataset_from_json") as mock_load_dataset, + patch("cli.alora.train.SafeSaveTrainer"), + patch("cli.alora.train.DataCollatorForCompletionOnlyLM"), + patch("cli.alora.train.os.makedirs"), + ): + # Setup tokenizer mock + mock_tokenizer = Mock() + custom_tokens = [111, 222, 333, 444] + mock_tokenizer.encode.return_value = custom_tokens + mock_tokenizer.eos_token = "" + mock_tokenizer_class.from_pretrained.return_value = mock_tokenizer + + # Setup other mocks + mock_model_class.from_pretrained.return_value = Mock() + mock_get_peft_model.return_value = Mock() + + mock_ds = MagicMock() + mock_ds.shuffle.return_value = mock_ds + mock_ds.select.return_value = mock_ds + mock_ds.__len__ = Mock(return_value=10) + mock_load_dataset.return_value = mock_ds + + # Call with custom invocation prompt + train_model( + dataset_path="test.jsonl", + base_model="test-model", + output_file="./test_output/adapter", + adapter="alora", + epochs=1, + ) + + # Verify tokenizer.encode was called with the invocation prompt + assert mock_tokenizer.encode.called, "Tokenizer encode should be called" + + # Verify the config has the correct tokens + peft_config = mock_get_peft_model.call_args[0][1] + assert peft_config.alora_invocation_tokens == custom_tokens, ( + "Config should have the tokenized invocation prompt" + ) + + +def test_imports_work(): + """Test that PEFT imports work correctly (no IBM alora dependency).""" + # This test verifies the migration was successful + from peft import LoraConfig, get_peft_model + + # Verify we can create a LoraConfig with alora_invocation_tokens + config = LoraConfig( + r=32, lora_alpha=32, task_type="CAUSAL_LM", alora_invocation_tokens=[1, 2, 3] + ) + + assert config.alora_invocation_tokens == [1, 2, 3], ( + "LoraConfig should support alora_invocation_tokens parameter" + ) + + # Verify get_peft_model is available + assert callable(get_peft_model), "get_peft_model should be callable" diff --git a/test/cli/test_alora_train_integration.py b/test/cli/test_alora_train_integration.py new file mode 100644 index 00000000..583aaa4c --- /dev/null +++ b/test/cli/test_alora_train_integration.py @@ -0,0 +1,353 @@ +"""Integration test for aLoRA/LoRA training with PEFT 0.18+. + +This test actually trains a tiny adapter to verify the migration works end-to-end. +""" + +import json +import os +import shutil +import sys +import tempfile +from pathlib import Path + +import pytest +import torch +from transformers import AutoTokenizer + +# Check if MPS is available but PyTorch version is too old +_mps_needs_cpu_fallback = torch.backends.mps.is_available() and tuple( + int(x) for x in torch.__version__.split(".")[:2] +) < (2, 8) + + +@pytest.mark.huggingface +@pytest.mark.llm +def test_alora_training_integration(): + """Integration test: Train a tiny aLoRA adapter and verify it works. + + This test: + 1. Creates a minimal training dataset (5 samples) + 2. Trains an aLoRA adapter for 1 epoch using a small model + 3. Verifies adapter files are created with correct PEFT 0.18+ format + 4. Cleans up temporary files + + Uses ibm-granite/granite-4.0-micro (smallest Granite model, 3B params). + """ + from cli.alora.train import train_model + + # Force CPU if MPS is available but PyTorch is too old + if _mps_needs_cpu_fallback: + import os + + # Disable MPS entirely to force CPU usage + os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "0" + print( + "⚠️ Warning: MPS available but PyTorch < 2.8.0. " + "Disabling MPS to run on CPU and avoid gradient scaling issues." + ) + + # Create temporary directory for test artifacts + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir_path = Path(tmpdir) + + # Create minimal training dataset (5 samples) + dataset_path = tmpdir_path / "train.jsonl" + training_data = [ + {"item": "Flywheel imbalance detected.", "label": "flywheel"}, + {"item": "Connecting rod bent.", "label": "connecting rod"}, + {"item": "Piston crown cracked.", "label": "piston"}, + {"item": "Oil seepage around rings.", "label": "piston rings"}, + {"item": "Carburetor obstructed.", "label": "mini-carburetor"}, + ] + + with open(dataset_path, "w") as f: + for item in training_data: + f.write(json.dumps(item) + "\n") + + # Output path for adapter + adapter_path = tmpdir_path / "test_alora_adapter" + + # Train aLoRA adapter with minimal settings + # Using smallest Granite model: granite-4.0-micro (3B params) + train_model( + dataset_path=str(dataset_path), + base_model="ibm-granite/granite-4.0-micro", + output_file=str(adapter_path), + adapter="alora", + epochs=1, # Just 1 epoch for speed + learning_rate=6e-6, + batch_size=1, # Minimal batch size + max_length=512, # Shorter sequences + grad_accum=1, # No gradient accumulation + ) + + # Verify adapter files were created + assert adapter_path.exists(), "Adapter directory should be created" + + adapter_config_path = adapter_path / "adapter_config.json" + assert adapter_config_path.exists(), "adapter_config.json should exist" + + # Verify adapter config has PEFT 0.18+ format + with open(adapter_config_path) as f: + config = json.load(f) + + # Key verification: PEFT 0.18+ uses "LORA" with alora_invocation_tokens + assert config.get("peft_type") == "LORA", ( + "PEFT 0.18+ uses peft_type='LORA' for aLoRA" + ) + + assert "alora_invocation_tokens" in config, ( + "Config should have alora_invocation_tokens (PEFT 0.18+ format)" + ) + + assert isinstance(config["alora_invocation_tokens"], list), ( + "alora_invocation_tokens should be a list of token IDs" + ) + + assert len(config["alora_invocation_tokens"]) > 0, ( + "alora_invocation_tokens should not be empty" + ) + + # Verify it does NOT have old IBM format + assert "invocation_string" not in config, ( + "Config should NOT have invocation_string (old IBM format)" + ) + + # Verify config field values match training parameters + assert config.get("r") == 32, "LoRA rank should be 32" + assert config.get("lora_alpha") == 32, "LoRA alpha should be 32" + assert config.get("lora_dropout") == 0.05, "LoRA dropout should be 0.05" + assert config.get("task_type") == "CAUSAL_LM", "Task type should be CAUSAL_LM" + + # Verify target modules + target_modules = config.get("target_modules", []) + assert "q_proj" in target_modules, "Should target q_proj" + assert "k_proj" in target_modules, "Should target k_proj" + assert "v_proj" in target_modules, "Should target v_proj" + + print("✅ Config field values verified") + + # Verify other expected files exist and check adapter weights + weights_file = None + if (adapter_path / "adapter_model.safetensors").exists(): + weights_file = adapter_path / "adapter_model.safetensors" + elif (adapter_path / "adapter_model.bin").exists(): + weights_file = adapter_path / "adapter_model.bin" + else: + raise AssertionError("Adapter weights file should exist") + + # Load and verify adapter weights + if weights_file.suffix == ".safetensors": + from safetensors.torch import load_file + + weights = load_file(str(weights_file)) + else: + weights = torch.load(weights_file) + + # Verify we have LoRA weight keys + lora_a_keys = [k for k in weights.keys() if "lora_A" in k] + lora_b_keys = [k for k in weights.keys() if "lora_B" in k] + assert len(lora_a_keys) > 0, "Should have lora_A weights" + assert len(lora_b_keys) > 0, "Should have lora_B weights" + + # Verify weights are non-zero (adapter actually trained) + for key, tensor in weights.items(): + assert tensor.abs().sum() > 0, f"Weight {key} should not be all zeros" + + # Verify weight shapes match rank (r=32) + for key in lora_a_keys: + assert weights[key].shape[0] == 32, ( + f"{key} should have rank 32 in first dim" + ) + for key in lora_b_keys: + assert weights[key].shape[1] == 32, ( + f"{key} should have rank 32 in second dim" + ) + + print("✅ Adapter weights verified (non-zero, correct shapes)") + print("✅ Successfully trained aLoRA adapter with PEFT 0.18+") + print( + f"✅ Config format verified: {config.get('peft_type')} with alora_invocation_tokens" + ) + + # Additional verification: Verify invocation tokens are correct + # The default invocation prompt is "<|start_of_role|>check_requirement<|end_of_role|>" + tokenizer = AutoTokenizer.from_pretrained("ibm-granite/granite-4.0-micro") + default_invocation_prompt = "<|start_of_role|>check_requirement<|end_of_role|>" + expected_tokens = tokenizer.encode( + default_invocation_prompt, add_special_tokens=False + ) + + assert config["alora_invocation_tokens"] == expected_tokens, ( + f"Invocation tokens {config['alora_invocation_tokens']} should match " + f"tokenized '{default_invocation_prompt}': {expected_tokens}" + ) + + print(f"✅ Invocation tokens verified: {config['alora_invocation_tokens']}") + + # Verify we can load the adapter with PEFT + from peft import PeftModel + from transformers import AutoModelForCausalLM + + base_model = AutoModelForCausalLM.from_pretrained( + "ibm-granite/granite-4.0-micro", + device_map="auto", + torch_dtype=torch.bfloat16, + ) + + # Load the trained adapter + model_with_adapter = PeftModel.from_pretrained( + base_model, str(adapter_path), adapter_name="test_alora" + ) + + # Verify adapter is loaded + assert "test_alora" in model_with_adapter.peft_config, ( + "Adapter should be loaded in PEFT model" + ) + + # Verify the loaded config matches what we saved + loaded_config = model_with_adapter.peft_config["test_alora"] + assert str(loaded_config.peft_type) == "PeftType.LORA", ( + "Loaded adapter should have LORA peft_type (enum format)" + ) + assert hasattr(loaded_config, "alora_invocation_tokens"), ( + "Loaded config should have alora_invocation_tokens attribute" + ) + assert loaded_config.alora_invocation_tokens == expected_tokens, ( # type: ignore + "Loaded adapter should have correct invocation tokens" + ) + + print("✅ Successfully loaded adapter with PEFT and verified configuration") + + # Test actual inference with activation + # Generate text WITHOUT invocation tokens (adapter should NOT activate) + test_prompt_no_activation = "What is a flywheel?" + inputs_no_activation = tokenizer( + test_prompt_no_activation, return_tensors="pt" + ).to(model_with_adapter.device) + + with torch.no_grad(): + outputs_no_activation = model_with_adapter.generate( + **inputs_no_activation, max_new_tokens=20, do_sample=False + ) + response_no_activation = tokenizer.decode( + outputs_no_activation[0], skip_special_tokens=True + ) + + print(f"✅ Generated without activation: {response_no_activation[:100]}...") + + # Generate text WITH invocation tokens (adapter SHOULD activate) + test_prompt_with_activation = f"{default_invocation_prompt} What is a flywheel?" + inputs_with_activation = tokenizer( + test_prompt_with_activation, return_tensors="pt" + ).to(model_with_adapter.device) + + with torch.no_grad(): + outputs_with_activation = model_with_adapter.generate( + **inputs_with_activation, max_new_tokens=20, do_sample=False + ) + response_with_activation = tokenizer.decode( + outputs_with_activation[0], skip_special_tokens=True + ) + + print(f"✅ Generated with activation: {response_with_activation[:100]}...") + + # Verify both generations succeeded (non-empty responses) + assert len(response_no_activation) > len(test_prompt_no_activation), ( + "Should generate non-empty response without activation" + ) + assert len(response_with_activation) > len(test_prompt_with_activation), ( + "Should generate non-empty response with activation" + ) + + # Check if responses differ (proving activation had an effect) + # Note: With minimal training, responses might be identical + if response_no_activation == response_with_activation: + print( + "⚠️ Warning: Responses identical with/without activation " + "(expected with minimal training)" + ) + else: + print( + "✅ Responses differ with/without activation " + "(adapter activation confirmed)" + ) + + print( + "✅ Verified adapter activation: both with/without invocation tokens generate successfully" + ) + + +@pytest.mark.huggingface +@pytest.mark.llm +def test_lora_training_integration(): + """Integration test: Train a tiny standard LoRA adapter and verify it works. + + This test verifies standard LoRA (non-aLoRA) also works with the migration. + """ + from cli.alora.train import train_model + + # Force CPU if MPS is available but PyTorch is too old + if _mps_needs_cpu_fallback: + import os + + # Disable MPS entirely to force CPU usage + os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "0" + print( + "⚠️ Warning: MPS available but PyTorch < 2.8.0. " + "Disabling MPS to run on CPU and avoid gradient scaling issues." + ) + + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir_path = Path(tmpdir) + + # Create minimal training dataset + dataset_path = tmpdir_path / "train.jsonl" + training_data = [ + {"item": "Flywheel imbalance detected.", "label": "flywheel"}, + {"item": "Connecting rod bent.", "label": "connecting rod"}, + {"item": "Piston crown cracked.", "label": "piston"}, + ] + + with open(dataset_path, "w") as f: + for item in training_data: + f.write(json.dumps(item) + "\n") + + adapter_path = tmpdir_path / "test_lora_adapter" + + # Train standard LoRA adapter + train_model( + dataset_path=str(dataset_path), + base_model="ibm-granite/granite-4.0-micro", + output_file=str(adapter_path), + adapter="lora", # Standard LoRA, not aLoRA + epochs=1, + batch_size=1, + max_length=512, + grad_accum=1, + ) + + # Verify adapter files were created + assert adapter_path.exists(), "Adapter directory should be created" + + adapter_config_path = adapter_path / "adapter_config.json" + assert adapter_config_path.exists(), "adapter_config.json should exist" + + # Verify adapter config + with open(adapter_config_path) as f: + config = json.load(f) + + assert config.get("peft_type") == "LORA", ( + "Standard LoRA should have peft_type='LORA'" + ) + + # Standard LoRA should NOT have alora_invocation_tokens + assert ( + "alora_invocation_tokens" not in config + or config.get("alora_invocation_tokens") is None + ), "Standard LoRA should not have alora_invocation_tokens" + + print("✅ Successfully trained standard LoRA adapter with PEFT 0.18+") + print( + f"✅ Config format verified: {config.get('peft_type')} without alora_invocation_tokens" + ) diff --git a/uv.lock b/uv.lock index f974d5bc..9225e8d4 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'darwin'", @@ -209,20 +209,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, ] -[[package]] -name = "alora" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "peft" }, - { name = "transformers" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/af/f5/c88b856fbb53b8fccf472c387d758f5f84e47f44be17707cd26824810a39/alora-0.2.0.tar.gz", hash = "sha256:e71f2c69bd1813f69f540efeea210be192ecd871b1ba94906a2e00b428b8fcb3", size = 68868, upload-time = "2025-06-09T19:35:56.694Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/bb/10bdbade587a26d0f95c6c44456d491a9c8cc5d60b793ae797b77e61da7c/alora-0.2.0-py3-none-any.whl", hash = "sha256:5b90607062037dd6953c81f32ec9ef5fca088484100c72fb2c88a5e8c2545eba", size = 62989, upload-time = "2025-06-09T19:35:55.44Z" }, -] - [[package]] name = "annotated-doc" version = "0.0.4" @@ -3429,7 +3415,6 @@ dependencies = [ [package.optional-dependencies] all = [ { name = "accelerate" }, - { name = "alora" }, { name = "boto3" }, { name = "datasets" }, { name = "docling" }, @@ -3454,7 +3439,6 @@ docling = [ ] hf = [ { name = "accelerate" }, - { name = "alora" }, { name = "datasets" }, { name = "granite-common", extra = ["transformers"] }, { name = "outlines" }, @@ -3517,7 +3501,6 @@ notebook = [ [package.metadata] requires-dist = [ { name = "accelerate", marker = "extra == 'hf'", specifier = ">=1.9.0" }, - { name = "alora", marker = "extra == 'hf'", specifier = "==0.2.0" }, { name = "ansicolors" }, { name = "boto3", marker = "extra == 'litellm'" }, { name = "click", specifier = "<8.2.0" }, @@ -3545,7 +3528,7 @@ requires-dist = [ { name = "outlines", marker = "extra == 'hf'" }, { name = "outlines-core", marker = "extra == 'hf'", specifier = "==0.1.26" }, { name = "outlines-core", marker = "extra == 'vllm'", specifier = "==0.1.26" }, - { name = "peft", marker = "extra == 'hf'", specifier = ">=0.18.0" }, + { name = "peft", marker = "extra == 'hf'", specifier = ">=0.18.1" }, { name = "pillow" }, { name = "pydantic" }, { name = "requests", specifier = ">=2.32.3" }, @@ -5006,7 +4989,7 @@ wheels = [ [[package]] name = "peft" -version = "0.18.0" +version = "0.18.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "accelerate" }, @@ -5020,9 +5003,9 @@ dependencies = [ { name = "tqdm" }, { name = "transformers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/0c/f2938db546ac7fc961ab5917cd50fcf5d0d70b406de93e3faccaa504e152/peft-0.18.0.tar.gz", hash = "sha256:c81c80b2056ab40c23d58ef25f74daab417ac653970718589a11a8af28218588", size = 634141, upload-time = "2025-11-13T11:13:06.603Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/48/147b3ea999560b40a34fd78724c7777aa9d18409c2250bdcaf9c4f2db7fc/peft-0.18.1.tar.gz", hash = "sha256:2dd0d6bfce936d1850e48aaddbd250941c5c02fc8ef3237cd8fd5aac35e0bae2", size = 635030, upload-time = "2026-01-09T13:08:01.136Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/55/481bf25613d40ef53534f664deba7b138fe566356b6ca10304e2b3b2529c/peft-0.18.0-py3-none-any.whl", hash = "sha256:624f69ca6393b765ccc6734adda7ca57d80b238f0900a42c357d8b67a03d62ff", size = 556427, upload-time = "2025-11-13T11:13:03.664Z" }, + { url = "https://files.pythonhosted.org/packages/b3/14/b4e3f574acf349ae6f61f9c000a77f97a3b315b4bb6ad03791e79ae4a568/peft-0.18.1-py3-none-any.whl", hash = "sha256:0bf06847a3551e3019fc58c440cffc9a6b73e6e2962c95b52e224f77bbdb50f1", size = 556960, upload-time = "2026-01-09T13:07:55.865Z" }, ] [[package]]