Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@ pip install fms-hf-tuning[fms-accel]
`fms-acceleration` is a collection of plugins that packages that accelerate fine-tuning / training of large models, as part of the `fms-hf-tuning` suite. For more details on see [this section below](#fms-acceleration).

## Data format
We support two data formats:
We support the following data formats:

1. #### Pre-process the JSON/JSONL dataset
### 1. JSON formats with a single sequence and a specified response_template to use for masking on completion.

#### 1.1 Pre-process the JSON/JSONL dataset
Pre-process the JSON/JSONL dataset to contain a single sequence of each data instance containing input + Response. The trainer is configured to expect a response template as a string. For example, if one wants to prepare the `alpaca` format data to feed into this trainer, it is quite easy and can be done with the following code.

```python
Expand Down Expand Up @@ -87,7 +89,7 @@ The same way can be applied to any dataset, with more info can be found [here](h

Once the JSON is converted using the formatting function, pass the `dataset_text_field` containing the single sequence to the trainer.

2. #### Format JSON/JSONL on the fly
#### 1.2 Format JSON/JSONL on the fly
Pass a JSON/JSONL and a `data_formatter_template` to use the formatting function on the fly while tuning. The template should specify fields of JSON with `{{field}}`. While tuning, the data will be converted to a single sequence using the template.
JSON fields can contain alpha-numeric characters, spaces and the following special symbols - "." , "_", "-".

Expand All @@ -101,8 +103,20 @@ data_formatter_template: `### Input: {{input}} \n\n##Label: {{output}}`

Formatting will happen on the fly while tuning. The keys in template should match fields in JSON file. The `response template` corresponding to the above template will need to be supplied. in this case, `response template` = `\n## Label:`.

##### In conclusion, if using the reponse_template and single sequence, either the `data_formatter_template` argument or `dataset_text_field` needs to be supplied to the trainer.

### 2. JSONL with input and output fields (no response template)

Pass a JSONL containing fields "input" with source text and "output" with class labels. Pre-format the input as you see fit. The output field will simply be concatenated to the end of input to create single sequence, and input will be masked.

The "input" and "output" field names are mandatory and cannot be changed.

##### In conclusion, either the `data_formatter_template` argument or `dataset_text_field` needs to be supplied to the trainer.
Example: Train.jsonl

```
{"input": "### Input: Colorado is a state in USA ### Output:", "output": "USA : Location"}
{"input": "### Input: Arizona is also a state in USA ### Output:", "output": "USA : Location"}
```

## Supported Models

Expand Down
56 changes: 56 additions & 0 deletions tests/test_sft_trainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
MALFORMATTED_DATA,
MODEL_NAME,
TWITTER_COMPLAINTS_DATA,
TWITTER_COMPLAINTS_DATA_INPUT_OUTPUT,
TWITTER_COMPLAINTS_JSON_FORMAT,
)

Expand Down Expand Up @@ -724,3 +725,58 @@ def test_run_with_good_experimental_metadata():
additional_callbacks=[TrainerCallback()],
exp_metadata=metadata,
)


### Tests for pretokenized data
def test_pretokenized_dataset():
"""Ensure that we can provide a pretokenized dataset with input/output format."""
with tempfile.TemporaryDirectory() as tempdir:
train_args = copy.deepcopy(TRAIN_ARGS)
train_args.output_dir = tempdir
data_args = copy.deepcopy(DATA_ARGS)
data_args.dataset_text_field = None
data_args.response_template = None
data_args.training_data_path = TWITTER_COMPLAINTS_DATA_INPUT_OUTPUT
sft_trainer.train(MODEL_ARGS, data_args, train_args, PEFT_PT_ARGS)
_validate_training(tempdir)


@pytest.mark.parametrize(
"dataset_text_field,response_template",
[
("foo", None),
(None, "bar"),
],
)
def test_pretokenized_dataset_bad_args(dataset_text_field, response_template):
"""Ensure that we can't provide only dataset text field / response template for pretok data."""
with tempfile.TemporaryDirectory() as tempdir:
train_args = copy.deepcopy(TRAIN_ARGS)
train_args.output_dir = tempdir

data_args = copy.deepcopy(DATA_ARGS)
data_args.dataset_text_field = dataset_text_field
data_args.response_template = response_template
data_args.training_data_path = TWITTER_COMPLAINTS_DATA_INPUT_OUTPUT
# We should raise an error since we should not have a dataset text
# field or a response template if we have pretokenized data
with pytest.raises(ValueError):
sft_trainer.train(MODEL_ARGS, data_args, train_args, PEFT_PT_ARGS)


def test_pretokenized_dataset_wrong_format():
"""Ensure that we fail to generate data if the data is in the wrong format."""
with tempfile.TemporaryDirectory() as tempdir:
train_args = copy.deepcopy(TRAIN_ARGS)
train_args.output_dir = tempdir

data_args = copy.deepcopy(DATA_ARGS)
data_args.dataset_text_field = None
data_args.response_template = None
data_args.training_data_path = TWITTER_COMPLAINTS_DATA

# It would be best to handle this in a way that is more understandable; we might
# need to directly add validation prior to the dataset generation since datasets
# is essentially swallowing a KeyError here.
with pytest.raises(ValueError):
sft_trainer.train(MODEL_ARGS, data_args, train_args, PEFT_PT_ARGS)
166 changes: 94 additions & 72 deletions tests/utils/test_preprocessing_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from tuning.utils.preprocessing_utils import (
combine_sequence,
format_dataset,
get_data_trainer_kwargs,
get_data_collator,
get_formatted_dataset_with_single_sequence,
get_preprocessed_dataset,
load_hf_dataset_from_jsonl_file,
Expand All @@ -42,6 +42,24 @@ def test_combine_sequence(input_element, output_element, expected_res):
assert comb_seq == expected_res


@pytest.mark.parametrize(
"input_element,output_element,expected_res",
[
("foo ", "bar", "foo bar"),
("foo\n", "bar", "foo\nbar"),
("foo\t", "bar", "foo\tbar"),
("foo", "bar", "foo bar"),
],
)
def test_combine_sequence_adds_eos(input_element, output_element, expected_res):
"""Ensure that input / output elements are combined with correct whitespace handling."""
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
comb_seq = combine_sequence(input_element, output_element, tokenizer.eos_token)
expected_res += tokenizer.eos_token
assert isinstance(comb_seq, str)
assert comb_seq == expected_res


# Tests for loading the dataset from disk
def test_load_hf_dataset_from_jsonl_file():
input_field_name = "Tweet text"
Expand Down Expand Up @@ -108,80 +126,53 @@ def test_get_preprocessed_dataset(max_sequence_length):
assert key_lengths.pop() <= max_sequence_length


# Tests for fetching train args
@pytest.mark.parametrize(
"use_validation_data, collator_type, packing",
"packing, response_template, formatted_train_dataset, max_seq_length, expected_collator",
[
(True, None, True),
(False, None, True),
(True, DataCollatorForCompletionOnlyLM, False),
(False, DataCollatorForCompletionOnlyLM, False),
(
False,
"\n### Label:",
load_hf_dataset_from_jsonl_file(
TWITTER_COMPLAINTS_DATA,
input_field_name="Tweet text",
output_field_name="text_label",
),
1024,
DataCollatorForCompletionOnlyLM,
),
(
False,
None,
Dataset.from_list(
[
{
"input_ids": [9437, 29, 210],
"attention_mask": [1, 1, 1],
"labels": [1, 20, 30],
}
]
),
1024,
DataCollatorForSeq2Seq,
),
],
)
def test_get_trainer_kwargs_with_response_template_and_text_field(
use_validation_data, collator_type, packing
def test_get_data_collator(
packing,
Comment thread
Ssukriti marked this conversation as resolved.
response_template,
formatted_train_dataset,
max_seq_length,
expected_collator,
):
training_data_path = TWITTER_COMPLAINTS_DATA
validation_data_path = training_data_path if use_validation_data else None
# Expected columns in the raw loaded dataset for the twitter data
column_names = set(["Tweet text", "ID", "Label", "text_label", "output"])
trainer_kwargs = get_data_trainer_kwargs(
training_data_path=training_data_path,
validation_data_path=validation_data_path,
packing=packing,
response_template="\n### Label:",
max_sequence_length=100,
tokenizer=AutoTokenizer.from_pretrained(MODEL_NAME),
dataset_text_field="output",
"""Ensure that the correct collator type is fetched based on the data args"""
collator = get_data_collator(
packing,
response_template,
AutoTokenizer.from_pretrained(MODEL_NAME),
formatted_train_dataset,
max_seq_length,
)
assert len(trainer_kwargs) == 3
# If we are packing, we should not have a data collator
if collator_type is None:
assert trainer_kwargs["data_collator"] is None
else:
assert isinstance(trainer_kwargs["data_collator"], collator_type)

# We should only have a validation dataset if one is present
if validation_data_path is None:
assert trainer_kwargs["eval_dataset"] is None
else:
assert isinstance(trainer_kwargs["eval_dataset"], Dataset)
assert set(trainer_kwargs["eval_dataset"].column_names) == column_names

assert isinstance(trainer_kwargs["train_dataset"], Dataset)
assert set(trainer_kwargs["train_dataset"].column_names) == column_names


@pytest.mark.parametrize("use_validation_data", [True, False])
def test_get_trainer_kwargs_with_custom_masking(use_validation_data):
training_data_path = TWITTER_COMPLAINTS_DATA_INPUT_OUTPUT
validation_data_path = training_data_path if use_validation_data else None
# Expected columns in the raw loaded dataset for the twitter data
column_names = set(["input_ids", "attention_mask", "labels"])
trainer_kwargs = get_data_trainer_kwargs(
training_data_path=training_data_path,
validation_data_path=validation_data_path,
packing=False,
response_template=None,
max_sequence_length=100,
tokenizer=AutoTokenizer.from_pretrained(MODEL_NAME),
dataset_text_field=None,
)
assert len(trainer_kwargs) == 4
# If we are packing, we should not have a data collator
assert isinstance(trainer_kwargs["data_collator"], DataCollatorForSeq2Seq)

# We should only have a validation dataset if one is present
if validation_data_path is None:
assert trainer_kwargs["eval_dataset"] is None
else:
assert isinstance(trainer_kwargs["eval_dataset"], Dataset)
assert set(trainer_kwargs["eval_dataset"].column_names) == column_names

assert isinstance(trainer_kwargs["train_dataset"], Dataset)
assert set(trainer_kwargs["train_dataset"].column_names) == column_names
# Needed to sidestep TRL validation
assert trainer_kwargs["formatting_func"] is not None
assert isinstance(collator, expected_collator)


# Tests for validating data args
Expand All @@ -197,6 +188,14 @@ def test_get_trainer_kwargs_with_custom_masking(use_validation_data):
),
False,
),
# data formatter with no response template
(
configs.DataArguments(
training_data_path=TWITTER_COMPLAINTS_DATA,
data_formatter_template="### Input: {{input}} \n\n### Response: {{output}}",
),
False,
),
# response template with no dataset_text_field or formatter
(
configs.DataArguments(
Expand All @@ -205,9 +204,17 @@ def test_get_trainer_kwargs_with_custom_masking(use_validation_data):
),
False,
),
# JSON without input / output for no single sequence arguments
(
configs.DataArguments(
training_data_path=TWITTER_COMPLAINTS_DATA,
),
False,
),
],
)
def test_validate_args(data_args, packing):
"""Ensure that respective errors are thrown for incorrect data arguments"""
with pytest.raises(ValueError):
validate_data_args(data_args, packing)

Expand Down Expand Up @@ -255,12 +262,27 @@ def test_get_formatted_dataset_with_single_sequence(
data_formatter_template="### Text:{{input}} \n\n### Label: {{output}}",
)
),
# input/output JSON with masking on input
(
configs.DataArguments(
training_data_path=TWITTER_COMPLAINTS_DATA_INPUT_OUTPUT,
validation_data_path=TWITTER_COMPLAINTS_DATA_INPUT_OUTPUT,
)
),
],
)
def test_format_dataset(data_args):
"""Ensure that the train/eval data are properly formatted based on the data args / text field"""
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
train_set, eval_set, dataset_text_field = format_dataset(data_args, tokenizer)
train_set, eval_set, dataset_text_field = format_dataset(
data_args, tokenizer, max_seq_length=1024
)
assert isinstance(train_set, Dataset)
assert isinstance(eval_set, Dataset)
assert dataset_text_field in train_set.column_names
assert dataset_text_field in eval_set.column_names
if dataset_text_field is None:
column_names = set(["input_ids", "attention_mask", "labels"])
assert set(eval_set.column_names) == column_names
assert set(train_set.column_names) == column_names
else:
assert dataset_text_field in train_set.column_names
assert dataset_text_field in eval_set.column_names
10 changes: 8 additions & 2 deletions tuning/sft_trainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,14 @@ def train(
formatted_train_dataset,
formatted_validation_dataset,
dataset_text_field,
) = format_dataset(data_args, tokenizer)
data_collator = get_data_collator(packing, data_args.response_template, tokenizer)
) = format_dataset(data_args, tokenizer, max_seq_length)
data_collator = get_data_collator(
packing,
data_args.response_template,
tokenizer,
formatted_train_dataset,
max_seq_length,
)

if framework is not None and framework.requires_agumentation:
model, (peft_config,) = framework.augmentation(
Expand Down
Loading