Files
wehub-resource-sync 593b94c120
pytest / Unit Tests (push) Has been cancelled
pytest / Integration (integration_tests_a) (push) Has been cancelled
pytest / Integration (integration_tests_b) (push) Has been cancelled
pytest / Integration (integration_tests_c) (push) Has been cancelled
pytest / Integration (integration_tests_d) (push) Has been cancelled
pytest / Integration (integration_tests_e) (push) Has been cancelled
pytest / Integration (integration_tests_f) (push) Has been cancelled
pytest / Integration (integration_tests_g) (push) Has been cancelled
pytest / Integration (integration_tests_h) (push) Has been cancelled
pytest / Integration (integration_tests_i) (push) Has been cancelled
pytest / Integration (integration_tests_j) (push) Has been cancelled
pytest / Distributed (distributed_a) (push) Has been cancelled
pytest / Distributed (distributed_b) (push) Has been cancelled
pytest / Distributed (distributed_c) (push) Has been cancelled
pytest / Distributed (distributed_d) (push) Has been cancelled
pytest / Distributed (distributed_e) (push) Has been cancelled
pytest / Distributed (distributed_f) (push) Has been cancelled
pytest / Minimal Install (push) Has been cancelled
pytest / Event File (push) Has been cancelled
pytest (slow) / py-slow (push) Has been cancelled
Publish JSON Schema / publish-schema (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:49:20 +08:00

1515 lines
53 KiB
Python

from __future__ import annotations
import copy
import json
import os
import pathlib
from typing import Any
import numpy as np
import pandas as pd
import pytest
import torch
import yaml
import ludwig.error as ludwig_error
from ludwig.api import LudwigModel
from ludwig.constants import (
ADAPTER,
BACKEND,
BASE_MODEL,
BATCH_SIZE,
COMBINER,
EPOCHS,
EVAL_BATCH_SIZE,
GENERATION,
INPUT_FEATURES,
MERGE_ADAPTER_INTO_BASE_MODEL,
MODEL_ECD,
MODEL_LLM,
MODEL_TYPE,
OUTPUT_FEATURES,
POSTPROCESSOR,
PREPROCESSING,
PRETRAINED_ADAPTER_WEIGHTS,
PROGRESSBAR,
PROMPT,
QUANTIZATION,
TARGET_MODULES,
TRAINER,
TYPE,
)
from ludwig.globals import MODEL_FILE_NAME, MODEL_WEIGHTS_FILE_NAME
from ludwig.models.llm import LLM
from ludwig.schema.model_types.base import ModelConfig
from ludwig.utils.fs_utils import list_file_names_in_directory
from ludwig.utils.types import DataFrame
from tests.integration_tests.utils import category_feature, generate_data, text_feature
pytestmark = pytest.mark.llm
LOCAL_BACKEND = {"type": "local"}
TEST_MODEL_NAME = "hf-internal-testing/tiny-random-GPTJForCausalLM"
MAX_NEW_TOKENS_TEST_DEFAULT = 5
RAY_BACKEND = {
"type": "ray",
"processor": {
"parallelism": 1,
},
"trainer": {
"use_gpu": False,
"num_workers": 2,
"resources_per_worker": {
"CPU": 1,
"GPU": 0,
},
},
}
def get_num_non_empty_tokens(iterable):
"""Returns the number of non-empty tokens."""
return len(list(filter(bool, iterable)))
@pytest.fixture(scope="module")
def local_backend():
return LOCAL_BACKEND
@pytest.fixture(scope="module")
def ray_backend():
return RAY_BACKEND
def get_dataset():
data = [
{"review": "I loved this movie!", "output": "positive"},
{"review": "The food was okay, but the service was terrible.", "output": "negative"},
{"review": "I can't believe how rude the staff was.", "output": "negative"},
{"review": "This book was a real page-turner.", "output": "positive"},
{"review": "The hotel room was dirty and smelled bad.", "output": "negative"},
{"review": "I had a great experience at this restaurant.", "output": "positive"},
{"review": "The concert was amazing!", "output": "positive"},
{"review": "The traffic was terrible on my way to work this morning.", "output": "negative"},
{"review": "The customer service was excellent.", "output": "positive"},
{"review": "I was disappointed with the quality of the product.", "output": "negative"},
]
df = pd.DataFrame(data)
return df
def get_generation_config():
return {
"temperature": 0.1,
"top_p": 0.75,
"top_k": 40,
"num_beams": 4,
"max_new_tokens": MAX_NEW_TOKENS_TEST_DEFAULT,
}
def convert_preds(preds: DataFrame):
if isinstance(preds, pd.DataFrame):
return preds.to_dict(orient="list")
return preds.compute().to_dict(orient="list")
@pytest.mark.llm
@pytest.mark.parametrize(
"backend",
[
pytest.param(LOCAL_BACKEND, id="local"),
pytest.param(RAY_BACKEND, id="ray"),
],
)
def test_llm_text_to_text(tmpdir, backend, ray_cluster_4cpu):
"""Test that the LLM model can train and predict with text inputs and text outputs."""
input_features = [
{
"name": "Question",
"type": "text",
"encoder": {"type": "passthrough"},
}
]
output_features = [text_feature(output_feature=True, name="Answer", decoder={"type": "text_extractor"})]
csv_filename = os.path.join(tmpdir, "training.csv")
dataset_filename = generate_data(input_features, output_features, csv_filename, num_examples=20)
config = {
MODEL_TYPE: MODEL_LLM,
BASE_MODEL: TEST_MODEL_NAME,
GENERATION: get_generation_config(),
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
BACKEND: backend,
}
model = LudwigModel(config)
model.train(dataset=dataset_filename, output_directory=str(tmpdir), skip_save_processed_input=True)
preds, _ = model.predict(dataset=dataset_filename, output_directory=str(tmpdir), split="test")
preds = convert_preds(preds)
assert "Answer_predictions" in preds
assert "Answer_probabilities" in preds
assert "Answer_probability" in preds
assert "Answer_response" in preds
assert preds["Answer_predictions"]
assert preds["Answer_probabilities"]
assert preds["Answer_probability"]
assert preds["Answer_response"]
# Check that in-line generation parameters are used. Original prediction uses max_new_tokens = 5.
assert get_num_non_empty_tokens(preds["Answer_predictions"][0]) <= MAX_NEW_TOKENS_TEST_DEFAULT
original_max_new_tokens = model.model.generation.max_new_tokens
# This prediction uses max_new_tokens = 2.
preds, _ = model.predict(
dataset=dataset_filename,
output_directory=str(tmpdir),
split="test",
generation_config={"min_new_tokens": 2, "max_new_tokens": 3},
)
preds = convert_preds(preds)
print(preds["Answer_predictions"][0])
num_non_empty_tokens = get_num_non_empty_tokens(preds["Answer_predictions"][0])
assert 2 <= num_non_empty_tokens <= 3
# Check that the state of the model is unchanged.
assert model.model.generation.max_new_tokens == original_max_new_tokens
@pytest.mark.llm
@pytest.mark.parametrize(
"backend",
[
pytest.param(LOCAL_BACKEND, id="local"),
pytest.param(RAY_BACKEND, id="ray"),
],
)
def test_llm_zero_shot_classification(tmpdir, backend, ray_cluster_4cpu):
input_features = [
{
"name": "review",
"type": "text",
}
]
output_features = [
category_feature(
name="output",
preprocessing={
"fallback_label": "neutral",
},
# How can we avoid using r here for regex, since it is technically an implementation detail?
decoder={
"type": "category_extractor",
"match": {
"positive": {"type": "contains", "value": "positive"},
"neutral": {"type": "regex", "value": r"\bneutral\b"},
"negative": {"type": "contains", "value": "negative"},
},
},
)
]
df = get_dataset()
config = {
MODEL_TYPE: MODEL_LLM,
BASE_MODEL: TEST_MODEL_NAME,
GENERATION: get_generation_config(),
PROMPT: {"task": "This is a review of a restaurant. Classify the sentiment."},
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
BACKEND: backend,
}
model = LudwigModel(config)
model.train(dataset=df, output_directory=str(tmpdir), skip_save_processed_input=True)
prediction_df = pd.DataFrame(
[
{"review": "The food was amazing!", "output": "positive"},
{"review": "The service was terrible.", "output": "negative"},
{"review": "The food was okay.", "output": "neutral"},
]
)
preds, _ = model.predict(dataset=prediction_df, output_directory=str(tmpdir))
preds = convert_preds(preds)
assert preds
@pytest.mark.llm
@pytest.mark.parametrize(
"backend",
[
pytest.param(LOCAL_BACKEND, id="local"),
pytest.param(RAY_BACKEND, id="ray"),
],
)
def test_llm_few_shot_classification(tmpdir, backend, csv_filename, ray_cluster_4cpu):
input_features = [
text_feature(
output_feature=False,
name="body",
encoder={"type": "passthrough"}, # need to use the default encoder for LLMTextInputFeatureConfig
)
]
output_features = [
category_feature(
output_feature=True,
name="output",
preprocessing={
"fallback_label": "3",
},
decoder={
"type": "category_extractor",
"match": {
"1": {"type": "contains", "value": "1"},
"2": {"type": "contains", "value": "2"},
"3": {"type": "contains", "value": "3"},
"4": {"type": "contains", "value": "4"},
"5": {"type": "contains", "value": "5"},
},
},
)
]
config = {
MODEL_TYPE: MODEL_LLM,
BASE_MODEL: TEST_MODEL_NAME,
GENERATION: get_generation_config(),
PROMPT: {
"retrieval": {"type": "random", "k": 3},
"task": (
"Given the sample input, complete this sentence by replacing XXXX: The review rating is XXXX. "
"Choose one value in this list: [1, 2, 3, 4, 5]."
),
},
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
PREPROCESSING: {
"split": {TYPE: "fixed"},
},
BACKEND: {**backend, "cache_dir": str(tmpdir)},
}
dataset_path = generate_data(
input_features,
output_features,
filename=csv_filename,
num_examples=25,
nan_percent=0.1,
with_split=True,
)
df = pd.read_csv(dataset_path)
df["output"] = np.random.choice([1, 2, 3, 4, 5], size=len(df)).astype(str) # ensure labels match the feature config
df.to_csv(dataset_path, index=False)
model = LudwigModel(config)
model.train(dataset=dataset_path, output_directory=str(tmpdir), skip_save_processed_input=True)
# TODO: fix LLM model loading
# model = LudwigModel.load(os.path.join(results.output_directory, "model"), backend=backend)
preds, _ = model.predict(dataset=dataset_path)
preds = convert_preds(preds)
assert preds
def _prepare_finetuning_test(
csv_filename: str, finetune_strategy: str, backend: dict, adapter_args: dict
) -> tuple[dict, str]:
input_features = [text_feature(name="input", encoder={"type": "passthrough"})]
output_features = [text_feature(name="output")]
train_df = generate_data(input_features, output_features, filename=csv_filename, num_examples=25)
prediction_df = pd.DataFrame(
[
{"input": "The food was amazing!", "output": "positive"},
{"input": "The service was terrible.", "output": "negative"},
{"input": "The food was okay.", "output": "neutral"},
]
)
model_name = TEST_MODEL_NAME
if finetune_strategy == "adalora":
# Adalora isn't supported for GPT-J model types, so use tiny bart
model_name = "hf-internal-testing/tiny-random-BartModel"
elif finetune_strategy == "adaption_prompt":
# At the time of writing this test, Adaption Prompt fine-tuning is only supported for Llama models
model_name = "yujiepan/llama-2-tiny-random"
config = {
MODEL_TYPE: MODEL_LLM,
BASE_MODEL: model_name,
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
GENERATION: {"max_new_tokens": 64},
TRAINER: {
TYPE: "finetune",
BATCH_SIZE: "auto",
EVAL_BATCH_SIZE: "auto",
EPOCHS: 2,
},
BACKEND: backend,
}
if finetune_strategy is not None:
config[ADAPTER] = {
TYPE: finetune_strategy,
**adapter_args,
}
return train_df, prediction_df, config
def _finetune_strategy_requires_cuda(finetune_strategy_name: str, quantization_args: dict | None) -> bool:
"""This method returns whether a given finetine_strategy requires CUDA.
For all finetune strategies, except "qlora", the decision is based just on the name of the finetine_strategy; in the
case of qlora, if the quantization dictionary is non-empty (i.e., contains quantization specifications), then the
original finetine_strategy name of "lora" is interpreted as "qlora" and used in the lookup, based on the list of
finetine strategies requiring CUDA.
"""
cuda_only_finetune_strategy_names: list[str] = [
"prompt_tuning",
"prefix_tuning",
"p_tuning",
"qlora",
]
if finetune_strategy_name == "lora" and quantization_args:
finetune_strategy_name = "qlora"
return finetune_strategy_name in cuda_only_finetune_strategy_names
def _verify_lm_lora_finetuning_layers(
attention_layer: torch.nn.Module,
target_modules: set[str],
merge_adapter_into_base_model: bool,
model_weights_directory: str,
expected_lora_in_features: int,
expected_lora_out_features: int,
expected_file_names: list[str],
) -> None:
"""This method verifies that LoRA finetuning layers have correct types and shapes, depending on whether the
optional "model.merge_and_unload()" method (based on the "merge_adapter_into_base_model" directive) was
executed.
If merge_adapter_into_base_model is True, then all specified LoRA projection layers in the attention layer must
contain square weight matrices (with the dimensions expected_lora_in_features by expected_lora_in_features).
However, if merge_adapter_into_base_model is False, then the LoRA part of the attention layer must include Lora_A
and Lora_B children layers for each specified projection, such that the product of Lora_A and Lora_B is a square
matrix (with the dimensions expected_lora_in_features by expected_lora_in_features) for each specified projection.
"""
from peft.tuners.lora.layer import LoraLayer
expected_lora_num_features_orig: tuple[int] = (expected_lora_in_features, expected_lora_out_features)
file_names: list[str] = list_file_names_in_directory(directory_name=model_weights_directory)
assert set(file_names) == set(expected_file_names)
target_module_name: str
target_module_obj: LoraLayer | torch.nn.Linear
# Not providing default value to "getattr()" so that error is raised if incorrect projection layer name is supplied.
for target_module_name in target_modules:
target_module_obj = getattr(attention_layer, target_module_name)
if merge_adapter_into_base_model:
assert isinstance(target_module_obj, torch.nn.Linear)
else:
assert isinstance(target_module_obj, LoraLayer)
if merge_adapter_into_base_model:
# If LoRA A & B layers are merged, they must have no children layers, and projection matrices must be square.
for target_module_name in target_modules:
target_module_obj = getattr(attention_layer, target_module_name)
assert not list(target_module_obj.children())
assert (target_module_obj.in_features, target_module_obj.out_features) == (
expected_lora_in_features,
expected_lora_out_features,
)
else:
# If LoRA A & B layers are not merged, their children layers must be correctly-dimensioned projection matrices.
expected_lora_num_features: tuple[int]
target_named_children: dict[str, torch.nn.Module]
lora_matrix_name: str
idx: int
for target_module_name in target_modules:
target_module_obj = getattr(attention_layer, target_module_name)
target_named_children = dict(target_module_obj.named_children())
for idx, lora_matrix_name in enumerate(["lora_A", "lora_B"]):
assert isinstance(target_named_children[lora_matrix_name]["default"], torch.nn.Linear)
# LoRA A and B matrix dimensions are transposes of one another so that their product is square matrix.
expected_lora_num_features = (
expected_lora_num_features_orig
if idx % 2 == 0
else (expected_lora_num_features_orig[1], expected_lora_num_features_orig[0])
)
assert (
target_named_children[lora_matrix_name]["default"].in_features,
target_named_children[lora_matrix_name]["default"].out_features,
) == expected_lora_num_features
@pytest.mark.llm
def test_llm_qat_torchao_end_to_end(tmpdir, csv_filename):
"""End-to-end smoke test for torchao quantization-aware training (QAT) on an LLM.
Fine-tunes ``hf-internal-testing/tiny-random-GPTJForCausalLM`` for a single epoch with
``quantization.backend: torchao``, ``mode: int8_weight_only``, ``qat: true`` and verifies:
* QAT observers are inserted before training (``_torchao_qat_prepared`` is set after
``prepare_for_training``).
* Training completes without errors.
* Save applies the conversion — after ``model.save_pretrained`` runs, the saved
checkpoint reflects the quantized weights, the ``_torchao_quantized`` flag is set,
and the model is reloadable for inference.
Paired with ``adapter: lora`` because Ludwig requires an adapter whenever quantization
is active on a finetune trainer (matches the existing QLoRA integration test pattern).
"""
pytest.importorskip("torchao", reason="torchao required for QAT tests")
input_features = [text_feature(name="input", encoder={"type": "passthrough"})]
output_features = [text_feature(name="output")]
train_df = generate_data(input_features, output_features, filename=csv_filename, num_examples=12)
config = {
MODEL_TYPE: MODEL_LLM,
BASE_MODEL: TEST_MODEL_NAME,
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
GENERATION: {"max_new_tokens": 16},
TRAINER: {
TYPE: "finetune",
BATCH_SIZE: 2,
EVAL_BATCH_SIZE: 2,
EPOCHS: 1,
},
ADAPTER: {TYPE: "lora", "r": 4, "alpha": 8},
QUANTIZATION: {"backend": "torchao", "mode": "int8_weight_only", "qat": True},
BACKEND: LOCAL_BACKEND,
}
output_directory: str = str(tmpdir)
model_directory: pathlib.Path = pathlib.Path(output_directory) / "api_experiment_run" / MODEL_FILE_NAME
model = LudwigModel(config)
model.train(dataset=train_df, output_directory=output_directory, skip_save_processed_input=False)
# QAT observers should have been inserted before training.
assert getattr(model.model, "_torchao_qat_prepared", False), "QAT preparation did not run"
# Save-time conversion should have fired.
assert getattr(model.model, "_torchao_quantized", False), "save-time quantization conversion did not run"
# Reload and verify inference runs through the QAT-converted model.
reloaded = LudwigModel.load(str(model_directory), backend=LOCAL_BACKEND)
prediction_df = pd.DataFrame([{"input": "Hello world", "output": ""}])
preds, _ = reloaded.predict(dataset=prediction_df, output_directory=output_directory)
preds = convert_preds(preds)
assert preds
@pytest.mark.llm
def test_llm_multi_adapter_registration_and_merge(tmpdir, csv_filename):
"""End-to-end smoke test for the ``adapters:`` multi-adapter config.
Registers two named LoRA adapters on a tiny GPTJ, runs a single fine-tune epoch,
attaches a TIES-merged adapter built from both sources, and verifies that:
* all three adapters (``a``, ``b``, ``merged``) exist on the loaded model,
* the active adapter after init matches ``adapters.active`` (``merged``), and
* predictions can be generated through the merged adapter.
Uses ``hf-internal-testing/tiny-random-GPTJForCausalLM`` — the smallest practical
causal LM in the Ludwig test suite — to keep wall-time low even on CPU runners.
"""
import peft as _peft # noqa: F401 (fail the test early on minimal installs)
input_features = [text_feature(name="input", encoder={"type": "passthrough"})]
output_features = [text_feature(name="output")]
train_df = generate_data(input_features, output_features, filename=csv_filename, num_examples=12)
config = {
MODEL_TYPE: MODEL_LLM,
BASE_MODEL: TEST_MODEL_NAME,
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
GENERATION: {"max_new_tokens": 16},
TRAINER: {
TYPE: "finetune",
BATCH_SIZE: 2,
EVAL_BATCH_SIZE: 2,
EPOCHS: 1,
},
"adapters": {
"adapters": {
"adapter_a": {"type": "lora", "r": 4, "alpha": 8},
"adapter_b": {"type": "lora", "r": 4, "alpha": 8},
},
"merge": {
"name": "merged",
"sources": ["adapter_a", "adapter_b"],
"weights": [0.5, 0.5],
"combination_type": "ties",
"density": 0.5,
},
"active": "merged",
},
BACKEND: LOCAL_BACKEND,
}
output_directory: str = str(tmpdir)
model_directory: pathlib.Path = pathlib.Path(output_directory) / "api_experiment_run" / MODEL_FILE_NAME
model = LudwigModel(config)
model.train(dataset=train_df, output_directory=output_directory, skip_save_processed_input=False)
# All three named adapters must be present on the PEFT-wrapped model.
peft_adapters = set(model.model.model.peft_config.keys())
assert {"adapter_a", "adapter_b", "merged"}.issubset(peft_adapters), f"missing adapters: {peft_adapters}"
# The active adapter after initialization should be the merged one we requested.
active = model.model.model.active_adapter
if isinstance(active, (list, tuple, set)):
active = next(iter(active))
assert active == "merged", f"expected active=merged, got {active!r}"
# Reload round-trip: the saved model's PEFT dir should carry all three adapters.
reloaded = LudwigModel.load(str(model_directory), backend=LOCAL_BACKEND)
reloaded_peft_adapters = set(reloaded.model.model.peft_config.keys())
assert {"adapter_a", "adapter_b", "merged"}.issubset(reloaded_peft_adapters)
# Generation through the merged adapter should run to completion.
prediction_df = pd.DataFrame([{"input": "The food was amazing!", "output": ""}])
preds, _ = reloaded.predict(dataset=prediction_df, output_directory=output_directory)
preds = convert_preds(preds)
assert preds
# TODO(arnav): p-tuning and prefix tuning have errors when enabled that seem to stem from distributed training:
#
# prefix tuning:
# Sizes of tensors must match except in dimension 1. Expected size 320 but got size 32 for tensor number 1 in the list.
#
# p-tuning:
# 'PromptEncoder' object has no attribute 'mlp_head'
@pytest.mark.llm
@pytest.mark.parametrize(
"backend",
[
pytest.param(LOCAL_BACKEND, id="local"),
# TODO(Arnav): Re-enable once we can run tests on GPUs
# This is because fine-tuning requires Ray with a distributed strategy, and distributed
# training requires GPUs
# pytest.param(RAY_BACKEND, id="ray"),
],
)
@pytest.mark.parametrize(
"finetune_strategy,adapter_args",
[
pytest.param(
None,
{},
id="full",
),
pytest.param(
"lora",
{},
id="lora-defaults",
),
pytest.param(
"lora",
{"r": 4, "dropout": 0.1},
id="lora-modified-defaults",
),
pytest.param(
"lora",
{TARGET_MODULES: ["q_proj", "k_proj", "v_proj"]},
id="lora-target-modules",
),
pytest.param(
"lora",
{"use_rslora": True},
id="lora-rslora-enabled",
),
pytest.param(
"lora",
{"use_dora": True},
id="lora-dora-enabled",
),
pytest.param(
"lora",
{"use_rslora": True, "use_dora": True},
id="lora-rslora-and-dora-enabled",
),
pytest.param(
"lora",
{POSTPROCESSOR: {MERGE_ADAPTER_INTO_BASE_MODEL: True, PROGRESSBAR: True}},
id="lora_merged",
),
pytest.param(
"lora",
{POSTPROCESSOR: {MERGE_ADAPTER_INTO_BASE_MODEL: False}},
id="lora_not_merged",
),
pytest.param(
"adalora",
{},
id="adalora-defaults",
),
pytest.param(
"adalora",
{"init_r": 8, "beta1": 0.8},
id="adalora-modified-defaults",
),
pytest.param(
"adalora",
{POSTPROCESSOR: {MERGE_ADAPTER_INTO_BASE_MODEL: True, PROGRESSBAR: True}},
id="adalora_merged",
),
pytest.param(
"adalora",
{POSTPROCESSOR: {MERGE_ADAPTER_INTO_BASE_MODEL: False}},
id="adalora_not_merged",
),
# TODO: <Alex>02/21/2024: Disabling AdaptionPrompt (waiting for PEFT release to fix
# "TypeError: LlamaRotaryEmbedding.forward() missing 1 required positional argument: 'position_ids')"
# (this is reflected in https://github.com/ludwig-ai/ludwig/issues/3938).
# </Alex>
# pytest.param(
# "adaption_prompt",
# {},
# id="adaption_prompt-defaults",
# ),
# pytest.param(
# "adaption_prompt",
# {"adapter_len": 6, "adapter_layers": 1},
# id="adaption_prompt-modified-defaults",
# ),
pytest.param(
"ia3",
{},
id="ia3-defaults",
),
pytest.param(
"ia3",
{"init_ia3_weights": False},
id="ia3-modified-defaults",
),
# pytest.param(
# "prompt_tuning",
# {
# "num_virtual_tokens": 8,
# "prompt_tuning_init": "RANDOM",
# },
# id="prompt_tuning_init_random",
# ),
# pytest.param(
# "prompt_tuning",
# {
# "num_virtual_tokens": 8,
# "prompt_tuning_init": "TEXT",
# "prompt_tuning_init_text": "Classify if the review is positive, negative, or neutral: ",
# },
# id="prompt_tuning_init_text",
# ),
# pytest.param(
# "prefix_tuning",
# {
# "num_virtual_tokens": 8,
# },
# id="prefix_tuning",
# ),
# pytest.param(
# "p_tuning",
# {"num_virtual_tokens": 8, "encoder_reparameterization_type": "MLP"},
# id="p_tuning_mlp_reparameterization",
# ),
# pytest.param(
# "p_tuning",
# {"num_virtual_tokens": 8, "encoder_reparameterization_type": "LSTM"},
# id="p_tuning_lstm_reparameterization",
# ),
],
)
def test_llm_finetuning_strategies(tmpdir, csv_filename, backend, finetune_strategy, adapter_args):
train_df, prediction_df, config = _prepare_finetuning_test(csv_filename, finetune_strategy, backend, adapter_args)
output_directory: str = str(tmpdir)
model_directory: str = pathlib.Path(output_directory) / "api_experiment_run" / MODEL_FILE_NAME
model = LudwigModel(config)
model.train(dataset=train_df, output_directory=output_directory, skip_save_processed_input=False)
# Make sure we can load the saved model and then use it for predictions
model = LudwigModel.load(str(model_directory), backend=backend)
base_model = LLM(ModelConfig.from_dict(config))
assert not _compare_models(base_model, model.model)
preds, _ = model.predict(dataset=prediction_df, output_directory=output_directory)
preds = convert_preds(preds)
assert preds
@pytest.mark.llm
@pytest.mark.parametrize(
"finetune_strategy,adapter_args,quantization",
[
pytest.param(
"lora",
{},
{"bits": 4},
id="qlora-4bit",
),
pytest.param(
"lora",
{},
{"bits": 8},
id="qlora-8bit",
),
],
)
def test_llm_finetuning_strategies_quantized(tmpdir, csv_filename, finetune_strategy, adapter_args, quantization):
pytest.importorskip("bitsandbytes", reason="bitsandbytes required for quantization tests")
if (
_finetune_strategy_requires_cuda(finetune_strategy_name=finetune_strategy, quantization_args=quantization)
and not (torch.cuda.is_available() and torch.cuda.device_count()) > 0
):
pytest.skip("Skip: quantization requires GPU and none are available.")
backend = LOCAL_BACKEND
train_df, prediction_df, config = _prepare_finetuning_test(csv_filename, finetune_strategy, backend, adapter_args)
config["backend"] = backend
config[QUANTIZATION] = quantization
model = LudwigModel(config)
model.train(dataset=train_df, output_directory=str(tmpdir), skip_save_processed_input=False)
# Make sure we can load the saved model and then use it for predictions
model = LudwigModel.load(os.path.join(str(tmpdir), "api_experiment_run", MODEL_FILE_NAME))
base_model = LLM(ModelConfig.from_dict(config))
assert not _compare_models(base_model, model.model)
preds, _ = model.predict(dataset=prediction_df, output_directory=str(tmpdir))
preds = convert_preds(preds)
assert preds
@pytest.mark.llm
@pytest.mark.skipif(torch.cuda.device_count() == 0, reason="test requires at least 1 gpu")
@pytest.mark.skipif(not torch.cuda.is_available(), reason="test requires gpu support")
@pytest.mark.parametrize(
"finetune_strategy,adapter_args,quantization,error_raised",
[
pytest.param(
"lora",
{POSTPROCESSOR: {MERGE_ADAPTER_INTO_BASE_MODEL: False}},
{"bits": 4},
(
ImportError,
"Using `load_in_8bit=True` requires Accelerate: `pip install accelerate` and the latest version of bitsandbytes `pip install -i https://test.pypi.org/simple/ bitsandbytes` or pip install bitsandbytes` ",
),
id="qlora-4bit-not-merged",
),
pytest.param(
"lora",
{POSTPROCESSOR: {MERGE_ADAPTER_INTO_BASE_MODEL: True, PROGRESSBAR: True}},
{"bits": 8},
(
ImportError,
"Using `load_in_8bit=True` requires Accelerate: `pip install accelerate` and the latest version of bitsandbytes `pip install -i https://test.pypi.org/simple/ bitsandbytes` or pip install bitsandbytes` ",
),
id="qlora-8bit-merged",
),
pytest.param(
"lora",
{POSTPROCESSOR: {MERGE_ADAPTER_INTO_BASE_MODEL: False}},
{"bits": 8},
(
ImportError,
"Using `load_in_8bit=True` requires Accelerate: `pip install accelerate` and the latest version of bitsandbytes `pip install -i https://test.pypi.org/simple/ bitsandbytes` or pip install bitsandbytes` ",
),
id="qlora-8bit-not-merged",
),
],
)
def test_llm_lora_finetuning_merge_and_unload_quantized_accelerate_required(
csv_filename, finetune_strategy, adapter_args, quantization, error_raised
):
pytest.importorskip("bitsandbytes", reason="bitsandbytes required for quantization tests")
input_features: list[dict] = [text_feature(name="input", encoder={"type": "passthrough"})]
output_features: list[dict] = [text_feature(name="output")]
config: dict = {
MODEL_TYPE: MODEL_LLM,
BASE_MODEL: TEST_MODEL_NAME,
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
TRAINER: {
TYPE: "finetune",
BATCH_SIZE: 8,
EPOCHS: 2,
},
ADAPTER: {
TYPE: finetune_strategy,
**adapter_args,
},
QUANTIZATION: quantization,
}
model = LudwigModel(config)
error_class: type
error_message: str
error_class, error_message = error_raised
with pytest.raises(error_class) as excinfo:
train_df = generate_data(input_features, output_features, filename=csv_filename, num_examples=3)
model.train(dataset=train_df)
assert str(excinfo.value) == error_message
@pytest.mark.llm
def test_llm_lora_finetuning_merge_and_unload_4_bit_quantization_not_supported(local_backend: dict):
input_features: list[dict] = [text_feature(name="input", encoder={"type": "passthrough"})]
output_features: list[dict] = [text_feature(name="output")]
finetune_strategy: str = "lora"
config: dict = {
MODEL_TYPE: MODEL_LLM,
BASE_MODEL: TEST_MODEL_NAME,
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
TRAINER: {
TYPE: "finetune",
BATCH_SIZE: 8,
EPOCHS: 2,
},
ADAPTER: {
TYPE: finetune_strategy,
POSTPROCESSOR: {MERGE_ADAPTER_INTO_BASE_MODEL: True, PROGRESSBAR: True},
},
QUANTIZATION: {"bits": 4},
BACKEND: local_backend,
}
expected_error_class: type = ludwig_error.ConfigValidationError
expected_error_message: str = """This operation will entail merging LoRA layers on a 4-bit quantized model. \
Calling "save_pretrained()" on that model is currently unsupported. If you want to merge the LoRA adapter weights \
into the base model, you need to use 8-bit quantization or do non-quantized based training by removing the \
quantization section from your Ludwig configuration."""
with pytest.raises(expected_error_class) as excinfo:
_ = LudwigModel(config)
assert str(excinfo.value) == expected_error_message
@pytest.mark.llm
@pytest.mark.parametrize(
"backend",
[
pytest.param(LOCAL_BACKEND, id="local"),
# TODO: Re-enable once we can run tests on GPUs
# This is because fine-tuning requires Ray with a distributed strategy, and distributed
# training requires GPUs
# pytest.param(RAY_BACKEND, id="ray"),
],
)
@pytest.mark.parametrize(
"target_modules,merge_adapter_into_base_model,expected_lora_in_features,expected_lora_out_features,expected_file_names",
[
pytest.param(
None,
False,
32,
8,
[
"README.md",
"adapter_config.json",
"adapter_model.safetensors",
],
id="lora_default_not_merged",
),
pytest.param(
None,
True,
32,
32,
[
"README.md",
"adapter_config.json",
"adapter_model.safetensors",
"config.json",
"generation_config.json",
"model.safetensors",
"tokenizer.json",
"tokenizer_config.json",
],
id="lora_default_merged",
),
pytest.param(
["q_proj", "k_proj", "v_proj"],
False,
32,
8,
[
"README.md",
"adapter_config.json",
"adapter_model.safetensors",
],
id="lora_custom_not_merged",
),
pytest.param(
["q_proj", "k_proj", "v_proj"],
True,
32,
32,
[
"README.md",
"adapter_config.json",
"adapter_model.safetensors",
"config.json",
"generation_config.json",
"model.safetensors",
"tokenizer.json",
"tokenizer_config.json",
],
id="lora_custom_merged",
),
],
)
def test_llm_lora_finetuning_merge_and_unload(
tmpdir: str,
csv_filename: str,
backend: dict,
target_modules: list[str] | set[str] | None,
merge_adapter_into_base_model: bool,
expected_lora_in_features: int,
expected_lora_out_features: int,
expected_file_names: list[str],
):
from peft.tuners.lora.config import LoraConfig
from peft.tuners.lora.model import LoraModel
finetune_strategy: str = "lora"
adapter_args: dict = {
POSTPROCESSOR: {
MERGE_ADAPTER_INTO_BASE_MODEL: merge_adapter_into_base_model,
},
}
# If "target_modules" is None, then ["q_proj", "v_proj"] is used (HuggingFace Transformers/PEFT internal default).
if target_modules:
adapter_args[TARGET_MODULES] = target_modules
train_df, prediction_df, config = _prepare_finetuning_test(
csv_filename=csv_filename, finetune_strategy=finetune_strategy, backend=backend, adapter_args=adapter_args
)
output_directory: str = str(tmpdir)
model_directory: str = pathlib.Path(output_directory) / "api_experiment_run" / MODEL_FILE_NAME
model_weights_directory: str = (
pathlib.Path(output_directory) / "api_experiment_run" / MODEL_FILE_NAME / MODEL_WEIGHTS_FILE_NAME
)
model = LudwigModel(config)
model.train(dataset=train_df, output_directory=output_directory, skip_save_processed_input=False)
# Get actual "target_modules" from trained model (to be used in assertions).
lora_model: LoraModel = model.model.model.base_model
peft_config: dict = lora_model.peft_config
lora_config: LoraConfig = peft_config["default"]
target_modules = lora_config.target_modules
_verify_lm_lora_finetuning_layers(
attention_layer=model.model.model.base_model.model.transformer.h[1].attn,
target_modules=target_modules,
merge_adapter_into_base_model=merge_adapter_into_base_model,
model_weights_directory=model_weights_directory,
expected_lora_in_features=expected_lora_in_features,
expected_lora_out_features=expected_lora_out_features,
expected_file_names=expected_file_names,
)
# Make sure we can load the saved model and verify that the LoRA layers have expected shapes.
model = LudwigModel.load(str(model_directory), backend=backend)
_verify_lm_lora_finetuning_layers(
attention_layer=model.model.model.base_model.model.transformer.h[1].attn,
target_modules=target_modules,
merge_adapter_into_base_model=merge_adapter_into_base_model,
model_weights_directory=model_weights_directory,
expected_lora_in_features=expected_lora_in_features,
expected_lora_out_features=expected_lora_out_features,
expected_file_names=expected_file_names,
)
@pytest.mark.llm
@pytest.mark.parametrize("use_adapter", [True, False], ids=["with_adapter", "without_adapter"])
def test_llm_training_with_gradient_checkpointing(tmpdir, csv_filename, use_adapter):
input_features = [text_feature(name="input", encoder={"type": "passthrough"})]
output_features = [text_feature(name="output")]
df = generate_data(input_features, output_features, filename=csv_filename, num_examples=25)
config = {
MODEL_TYPE: MODEL_LLM,
BASE_MODEL: "hf-internal-testing/tiny-random-BartModel",
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
TRAINER: {
TYPE: "finetune",
BATCH_SIZE: 8,
EPOCHS: 1,
"enable_gradient_checkpointing": True,
},
}
if use_adapter:
config[ADAPTER] = {TYPE: "lora"}
model = LudwigModel(config)
assert model.config_obj.trainer.enable_gradient_checkpointing
model.train(dataset=df, output_directory=str(tmpdir), skip_save_processed_input=False)
@pytest.mark.llm
def test_lora_wrap_on_init():
from peft import PeftModel
from transformers import PreTrainedModel
config = {
MODEL_TYPE: MODEL_LLM,
BASE_MODEL: TEST_MODEL_NAME,
INPUT_FEATURES: [text_feature(name="input", encoder={"type": "passthrough"})],
OUTPUT_FEATURES: [text_feature(name="output")],
TRAINER: {
TYPE: "finetune",
BATCH_SIZE: 8,
EPOCHS: 2,
},
}
config_obj = ModelConfig.from_dict(config)
model = LLM(config_obj)
assert isinstance(model.model, PreTrainedModel)
assert not isinstance(model.model, PeftModel)
# Now add adapter
config[ADAPTER] = {
TYPE: "lora",
}
config_obj = ModelConfig.from_dict(config)
model = LLM(config_obj)
# We need to explicitly make this call since we now load the adapter
# in the trainer as opposed to the point of LLM model initialization.
model.prepare_for_training()
assert not isinstance(model.model, PreTrainedModel)
assert isinstance(model.model, PeftModel)
def test_llama_rope_scaling():
config = {
MODEL_TYPE: MODEL_LLM,
BASE_MODEL: "HuggingFaceH4/tiny-random-LlamaForCausalLM",
INPUT_FEATURES: [text_feature(name="input", encoder={"type": "passthrough"})],
OUTPUT_FEATURES: [text_feature(name="output")],
TRAINER: {
TYPE: "finetune",
BATCH_SIZE: 8,
EPOCHS: 2,
},
"model_parameters": {
"rope_scaling": {
"rope_type": "dynamic",
"factor": 2.0,
}
},
}
config_obj = ModelConfig.from_dict(config)
model = LLM(config_obj)
assert model.model.config.rope_scaling
assert model.model.config.rope_scaling["rope_type"] == "dynamic"
assert model.model.config.rope_scaling["factor"] == 2.0
def test_default_max_sequence_length():
config = {
MODEL_TYPE: MODEL_LLM,
BASE_MODEL: TEST_MODEL_NAME,
INPUT_FEATURES: [text_feature(name="input", encoder={"type": "passthrough"})],
OUTPUT_FEATURES: [text_feature(name="output")],
TRAINER: {
TYPE: "finetune",
BATCH_SIZE: 8,
EPOCHS: 2,
},
ADAPTER: {TYPE: "lora", PRETRAINED_ADAPTER_WEIGHTS: "Infernaught/test_adapter_weights"},
BACKEND: {TYPE: "local"},
}
config_obj = ModelConfig.from_dict(config)
assert config_obj.input_features[0].preprocessing.max_sequence_length is None
assert config_obj.output_features[0].preprocessing.max_sequence_length is None
@pytest.mark.llm
@pytest.mark.parametrize(
"adapter",
[
"lora",
"adalora",
# TODO: <Alex>02/21/2024: Disabling AdaptionPrompt (waiting for PEFT release to fix
# "TypeError: LlamaRotaryEmbedding.forward() missing 1 required positional argument: 'position_ids')"
# (this is reflected in https://github.com/ludwig-ai/ludwig/issues/3938).
# </Alex>
# "adaption_prompt",
],
)
def test_load_pretrained_adapter_weights(adapter):
from peft import PeftModel
from transformers import PreTrainedModel
if adapter == "lora":
weights = "Infernaught/test_adapter_weights"
base_model = TEST_MODEL_NAME
elif adapter == "adalora":
weights = "Infernaught/test_adalora_weights"
base_model = "HuggingFaceH4/tiny-random-LlamaForCausalLM"
elif adapter == "adaption_prompt":
weights = "Infernaught/test_ap_weights"
base_model = "HuggingFaceH4/tiny-random-LlamaForCausalLM"
else:
raise ()
config = {
MODEL_TYPE: MODEL_LLM,
BASE_MODEL: base_model,
INPUT_FEATURES: [text_feature(name="input", encoder={"type": "passthrough"})],
OUTPUT_FEATURES: [text_feature(name="output")],
TRAINER: {
TYPE: "none",
BATCH_SIZE: 8,
EPOCHS: 2,
},
ADAPTER: {TYPE: adapter, PRETRAINED_ADAPTER_WEIGHTS: weights},
BACKEND: {TYPE: "local"},
}
config_obj = ModelConfig.from_dict(config)
model = LLM(config_obj)
assert model.config_obj.adapter.pretrained_adapter_weights
assert model.config_obj.adapter.pretrained_adapter_weights == weights
model.prepare_for_training()
assert not isinstance(model.model, PreTrainedModel)
assert isinstance(model.model, PeftModel)
config_obj = ModelConfig.from_dict(config)
assert config_obj.input_features[0].preprocessing.max_sequence_length is None
assert config_obj.output_features[0].preprocessing.max_sequence_length is None
def _compare_models(model_1: torch.nn.Module, model_2: torch.nn.Module) -> bool:
# For a full explanation of this 8-bit workaround, see https://github.com/ludwig-ai/ludwig/pull/3606
# TODO: Uncomment "filter_for_weight_format()" method definition and enable its usage once GPU tests are set up.
# def filter_for_weight_format(i):
# """Remove bitsandbytes metadata keys added on state dict creation.
#
# 8-bit quantized models that have been put on gpu will have a set of `weight_format` keys in their state dict.
# These contain strings that are used to reshape quantized tensors, however these have no impact until the state
# dict is loaded into a model. These keys were causing `torch.equal` to raise an exception, so we skip them in
# the evaluation.
# """
# return "weight_format" not in i[0]
# model_1_filtered_state_dict = filter(filter_for_weight_format, model_1.state_dict().items())
# model_2_filtered_state_dict = filter(filter_for_weight_format, model_2.state_dict().items())
# Source: https://discuss.pytorch.org/t/check-if-models-have-same-weights/4351/6
if model_1.__class__.__name__ != model_2.__class__.__name__:
return False
if (
hasattr(model_1, "model")
and hasattr(model_2, "model")
and not _compare_models(model_1=model_1.model, model_2=model_2.model)
):
return False
for key_item_1, key_item_2 in zip(model_1.state_dict().items(), model_2.state_dict().items()):
if not torch.equal(key_item_1[1], key_item_2[1]):
return False
return True
def test_global_max_sequence_length_for_llms():
"""Ensures that user specified global_max_sequence_length can never be greater than the model's context
length."""
config = {
MODEL_TYPE: MODEL_LLM,
BASE_MODEL: "HuggingFaceH4/tiny-random-LlamaForCausalLM",
INPUT_FEATURES: [text_feature(name="input", encoder={"type": "passthrough"})],
OUTPUT_FEATURES: [text_feature(name="output")],
}
config_obj = ModelConfig.from_dict(config)
model = LLM(config_obj)
# Default value is set based on model's context_len
assert model.global_max_sequence_length == 2048
# Override to a larger value in the config
config["preprocessing"] = {"global_max_sequence_length": 4096}
config_obj = ModelConfig.from_dict(config)
model = LLM(config_obj)
# Check that the value can never be larger than the model's context_len
assert model.global_max_sequence_length == 2048
def test_local_path_loading(tmpdir):
"""Tests that local paths can be used to load models."""
from huggingface_hub import snapshot_download
# Download the model to a local directory
local_path: str = f"{tmpdir!s}/test_local_path_loading"
repo_id: str = "HuggingFaceH4/tiny-random-LlamaForCausalLM"
os.makedirs(local_path, exist_ok=True)
snapshot_download(repo_id=repo_id, local_dir=local_path)
# Load the model using the local path
config1 = {
MODEL_TYPE: MODEL_LLM,
BASE_MODEL: local_path,
INPUT_FEATURES: [text_feature(name="input", encoder={"type": "passthrough"})],
OUTPUT_FEATURES: [text_feature(name="output")],
}
config_obj1 = ModelConfig.from_dict(config1)
model1 = LLM(config_obj1)
# Load the model using the repo id
config2 = {
MODEL_TYPE: MODEL_LLM,
BASE_MODEL: repo_id,
INPUT_FEATURES: [text_feature(name="input", encoder={"type": "passthrough"})],
OUTPUT_FEATURES: [text_feature(name="output")],
}
config_obj2 = ModelConfig.from_dict(config2)
model2 = LLM(config_obj2)
# Check that the models are the same
assert _compare_models(model1.model, model2.model)
@pytest.mark.parametrize(
"finetuning_strategy, embedding_noise",
[
pytest.param(None, 0, id="full_finetuning_without_noise"),
pytest.param(None, 5, id="full_finetuning_with_noise"),
pytest.param("lora", 0, id="lora_without_noise"),
pytest.param("lora", 5, id="lora_with_noise"),
],
)
def test_llm_finetuning_with_embedding_noise(
tmpdir,
csv_filename,
finetuning_strategy,
embedding_noise,
):
train_df, prediction_df, config = _prepare_finetuning_test(csv_filename, finetuning_strategy, LOCAL_BACKEND, {})
# Add embedding noise
if embedding_noise:
config["model_parameters"] = {"neftune_noise_alpha": embedding_noise}
model = LudwigModel(config)
if embedding_noise:
assert model.config_obj.model_parameters.neftune_noise_alpha == embedding_noise
output_directory: str = str(tmpdir)
model_directory: str = pathlib.Path(output_directory) / "api_experiment_run" / MODEL_FILE_NAME
model.train(dataset=train_df, output_directory=output_directory, skip_save_processed_input=False)
# Make sure we can load the saved model and then use it for predictions
model = LudwigModel.load(str(model_directory), backend=LOCAL_BACKEND)
base_model = LLM(ModelConfig.from_dict(config))
assert not _compare_models(base_model, model.model)
preds, _ = model.predict(dataset=prediction_df, output_directory=output_directory)
preds = convert_preds(preds)
assert preds
@pytest.fixture()
def llm_encoder_config() -> dict[str, Any]:
encoder_config = {
TYPE: "llm",
BASE_MODEL: "HuggingFaceH4/tiny-random-LlamaForCausalLM",
}
return encoder_config
@pytest.mark.parametrize(
"adapter,quantization",
[
(None, None),
("lora", None),
("lora", {"bits": 4}),
("lora", {"bits": 8}),
("adalora", None),
("adalora", {"bits": 4}),
("adalora", {"bits": 8}),
],
ids=["FFT", "LoRA", "LoRA 4-bit", "LoRA 8-bit", "AdaLoRA", "AdaLoRA 4-bit", "AdaLoRA 8-bit"],
)
def test_llm_encoding(llm_encoder_config, adapter, quantization, tmpdir):
if quantization:
pytest.importorskip("bitsandbytes", reason="bitsandbytes required for quantization tests")
if (
_finetune_strategy_requires_cuda(
finetune_strategy_name="lora" if adapter else None, quantization_args=quantization
)
and not (torch.cuda.is_available() and torch.cuda.device_count()) > 0
):
pytest.skip("Skip: quantization requires GPU and none are available.")
dataset_path = os.path.join(tmpdir, "llm_classification_data.csv")
config = {
MODEL_TYPE: MODEL_ECD,
OUTPUT_FEATURES: [category_feature(name="output")],
COMBINER: {TYPE: "sequence"},
TRAINER: {EPOCHS: 1},
}
encoder_config = copy.deepcopy(llm_encoder_config)
if adapter:
encoder_config[ADAPTER] = {TYPE: adapter}
if quantization:
encoder_config[QUANTIZATION] = quantization
config[BACKEND] = LOCAL_BACKEND
config[INPUT_FEATURES] = [text_feature(name="input", encoder=encoder_config)]
generate_data(input_features=config[INPUT_FEATURES], output_features=config[OUTPUT_FEATURES], filename=dataset_path)
model = LudwigModel(config)
model.train(dataset=dataset_path, output_directory=str(tmpdir))
def test_llm_batch_size_tuning():
dataset = pd.DataFrame({"instruction": ["a"] * 100, "output": ["a"] * 100})
config = yaml.safe_load("""
model_type: llm
input_features:
- name: instruction
type: text
output_features:
- name: output
type: text
prompt:
template: >-
{instruction}
adapter:
type: lora
trainer:
type: finetune
optimizer:
type: adam
batch_size: auto
train_steps: 1
learning_rate: 0.0002
eval_batch_size: 2
backend:
type: local
base_model: HuggingFaceH4/tiny-random-LlamaForCausalLM
""")
model = LudwigModel(config=config)
model.train(dataset=dataset)
assert model.config_obj.trainer.batch_size > 1
@pytest.mark.llm
def test_llm_used_tokens(tmpdir):
input_features = [text_feature(name="input", encoder={"type": "passthrough"})]
output_features = [text_feature(name="output")]
df = pd.read_json("https://raw.githubusercontent.com/sahil280114/codealpaca/master/data/code_alpaca_20k.json").head(
10
)
# df = generate_data(input_features, output_features, filename=csv_filename, num_examples=25)
config = {
MODEL_TYPE: MODEL_LLM,
BASE_MODEL: "hf-internal-testing/tiny-random-BartModel",
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
TRAINER: {
TYPE: "finetune",
BATCH_SIZE: 1,
EPOCHS: 3,
"enable_gradient_checkpointing": True,
},
}
config[ADAPTER] = {TYPE: "lora"}
model = LudwigModel(config)
assert model.config_obj.trainer.enable_gradient_checkpointing
model.train(dataset=df, output_directory=str(tmpdir), skip_save_processed_input=False)
with open(
os.path.join(str(tmpdir), "api_experiment_run", MODEL_FILE_NAME, "training_progress.json"), encoding="utf-8"
) as f:
progress_tracker = json.load(f)
assert progress_tracker["cumulative_step_token_usage"]["11"] == progress_tracker["total_tokens_used"] == 621
assert progress_tracker["checkpoint_to_epoch"] == {"1": 1, "2": 1, "3": 2, "4": 2, "5": 3, "6": 3}
assert progress_tracker["checkpoint_to_step"] == {"1": 4, "2": 4, "3": 8, "4": 8, "5": 12, "6": 12}
assert progress_tracker["cumulative_checkpoint_token_usage"] == {
"1": 207,
"2": 207,
"3": 414,
"4": 414,
"5": 621,
"6": 621,
}
assert progress_tracker["incremental_checkpoint_token_usage"] == {
"1": 207,
"2": 0,
"3": 207,
"4": 0,
"5": 207,
"6": 0,
}