mirror of
https://github.com/GoogleCloudPlatform/vertex-ai-samples.git
synced 2026-09-26 14:42:04 +00:00
Add vmg templates, dataset_validation_util and update common_util (#3586)
* Add vmg templates, dataset_validation_util and update common_util * Add name to CODEOWNERS * Update common_util.py --------- Co-authored-by: Rayan Dasoriya <dasoriya@google.com>
This commit is contained in:
co-authored by
Rayan Dasoriya
parent
30c3e627a7
commit
0727e19520
@@ -20,6 +20,7 @@
|
||||
/vertex_model_garden/model_oss/movinet @KCFindstr
|
||||
/vertex_model_garden/model_oss/data_converter @KCFindstr
|
||||
/vertex_model_garden/model_oss/peft @weigary
|
||||
/vertex_model_garden/model_oss/peft/templates @rayandasoriya
|
||||
/vertex_model_garden/model_oss/lm-evaluation-harness @kathyyu-google
|
||||
/vertex_model_garden/model_oss/tfvision @dstnluong-google
|
||||
/vertex_model_garden/model_oss/fvlm @minwoo33park
|
||||
|
||||
@@ -476,6 +476,7 @@ def get_resource_id(
|
||||
accelerator_type: str,
|
||||
is_for_training: bool,
|
||||
is_restricted_image: bool = False,
|
||||
is_dynamic_workload_scheduler: bool = False,
|
||||
) -> str:
|
||||
"""Returns the resource id for a given accelerator type and the use case.
|
||||
|
||||
@@ -484,45 +485,61 @@ def get_resource_id(
|
||||
is_for_training: Whether the resource is used for training. Set false for
|
||||
serving use case.
|
||||
is_restricted_image: Whether the image is hosted in `vertex-ai-restricted`.
|
||||
is_dynamic_workload_scheduler: Whether the resource is used with Dynamic
|
||||
Workload Scheduler.
|
||||
|
||||
Returns:
|
||||
The resource id.
|
||||
"""
|
||||
accelerator_suffix_map = {
|
||||
"NVIDIA_TESLA_V100": "nvidia_v100_gpus",
|
||||
"NVIDIA_L4": "nvidia_l4_gpus",
|
||||
"NVIDIA_TESLA_A100": "nvidia_a100_gpus",
|
||||
"NVIDIA_A100_80GB": "nvidia_a100_80gb_gpus",
|
||||
"NVIDIA_H100_80GB": "nvidia_h100_gpus",
|
||||
"NVIDIA_TESLA_T4": "nvidia_t4_gpus",
|
||||
"TPU_V5e": "tpu_v5e",
|
||||
"TPU_V3": "tpu_v3",
|
||||
}
|
||||
default_training_accelerator_map = {
|
||||
"NVIDIA_TESLA_V100": "custom_model_training_nvidia_v100_gpus",
|
||||
"NVIDIA_L4": "custom_model_training_nvidia_l4_gpus",
|
||||
"NVIDIA_TESLA_A100": "custom_model_training_nvidia_a100_gpus",
|
||||
"NVIDIA_A100_80GB": "custom_model_training_nvidia_a100_80gb_gpus",
|
||||
"NVIDIA_H100_80GB": "custom_model_training_nvidia_h100_gpus",
|
||||
"NVIDIA_TESLA_T4": "custom_model_training_nvidia_t4_gpus",
|
||||
"TPU_V5e": "custom_model_training_tpu_v5e",
|
||||
"TPU_V3": "custom_model_training_tpu_v3",
|
||||
key: f"custom_model_training_{accelerator_suffix_map[key]}"
|
||||
for key in accelerator_suffix_map
|
||||
}
|
||||
dws_training_accelerator_map = {
|
||||
key: f"custom_model_training_preemptible_{accelerator_suffix_map[key]}"
|
||||
for key in accelerator_suffix_map
|
||||
}
|
||||
restricted_image_training_accelerator_map = {
|
||||
"NVIDIA_A100_80GB": "restricted_image_training_nvidia_a100_80gb_gpus",
|
||||
}
|
||||
serving_accelerator_map = {
|
||||
"NVIDIA_TESLA_V100": "custom_model_serving_nvidia_v100_gpus",
|
||||
"NVIDIA_L4": "custom_model_serving_nvidia_l4_gpus",
|
||||
"NVIDIA_TESLA_A100": "custom_model_serving_nvidia_a100_gpus",
|
||||
"NVIDIA_A100_80GB": "custom_model_serving_nvidia_a100_80gb_gpus",
|
||||
"NVIDIA_H100_80GB": "custom_model_serving_nvidia_h100_gpus",
|
||||
"NVIDIA_TESLA_T4": "custom_model_serving_nvidia_t4_gpus",
|
||||
"TPU_V5e": "custom_model_serving_tpu_v5e",
|
||||
key: f"custom_model_serving_{accelerator_suffix_map[key]}"
|
||||
for key in accelerator_suffix_map
|
||||
}
|
||||
|
||||
if is_for_training:
|
||||
if is_restricted_image and is_dynamic_workload_scheduler:
|
||||
raise ValueError(
|
||||
"Dynamic Workload Scheduler does not work for restricted image"
|
||||
" training."
|
||||
)
|
||||
training_accelerator_map = (
|
||||
restricted_image_training_accelerator_map
|
||||
if is_restricted_image
|
||||
else default_training_accelerator_map
|
||||
)
|
||||
if accelerator_type in training_accelerator_map:
|
||||
return training_accelerator_map[accelerator_type]
|
||||
if is_dynamic_workload_scheduler:
|
||||
return dws_training_accelerator_map[accelerator_type]
|
||||
else:
|
||||
return training_accelerator_map[accelerator_type]
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Could not find accelerator type: {accelerator_type} for training."
|
||||
)
|
||||
else:
|
||||
if is_dynamic_workload_scheduler:
|
||||
raise ValueError("Dynamic Workload Scheduler does not work for serving.")
|
||||
if accelerator_type in serving_accelerator_map:
|
||||
return serving_accelerator_map[accelerator_type]
|
||||
else:
|
||||
@@ -538,10 +555,14 @@ def check_quota(
|
||||
accelerator_count: int,
|
||||
is_for_training: bool,
|
||||
is_restricted_image: bool = False,
|
||||
is_dynamic_workload_scheduler: bool = False,
|
||||
):
|
||||
"""Checks if the project and the region has the required quota."""
|
||||
resource_id = get_resource_id(
|
||||
accelerator_type, is_for_training, is_restricted_image
|
||||
accelerator_type,
|
||||
is_for_training=is_for_training,
|
||||
is_restricted_image=is_restricted_image,
|
||||
is_dynamic_workload_scheduler=is_dynamic_workload_scheduler,
|
||||
)
|
||||
quota = get_quota(project_id, region, resource_id)
|
||||
quota_request_instruction = (
|
||||
|
||||
+486
@@ -0,0 +1,486 @@
|
||||
"""Functions for dataset validation.
|
||||
|
||||
This tool is used to validate the dataset against the given template.
|
||||
"""
|
||||
|
||||
import json
|
||||
import multiprocessing
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Any, Callable, Dict, Union
|
||||
from absl import logging
|
||||
import accelerate
|
||||
import datasets
|
||||
import transformers
|
||||
|
||||
GCS_URI_PREFIX = "gs://"
|
||||
GCSFUSE_URI_PREFIX = "/gcs/"
|
||||
LOCAL_BASE_MODEL_DIR = "/tmp/base_model_dir"
|
||||
LOCAL_TEMPLATE_DIR = "/tmp/template_dir"
|
||||
_TEMPLATE_DIRNAME = "templates"
|
||||
_VERTEX_AI_SAMPLES_GITHUB_REPO_NAME = "vertex-ai-samples"
|
||||
_VERTEX_AI_SAMPLES_GITHUB_TEMPLATE_DIR = (
|
||||
"community-content/vertex_model_garden/model_oss/peft/templates"
|
||||
)
|
||||
_DESCRIPTION_KEY = "description"
|
||||
_SOURCE_KEY = "source"
|
||||
_PROMPT_INPUT_KEY = "prompt_input"
|
||||
_PROMPT_NO_INPUT_KEY = "prompt_no_input"
|
||||
_RESPONSE_SEPARATOR = "response_separator"
|
||||
_INSTRUCTION_SEPARATOR = "instruction_separator"
|
||||
_CHAT_TEMPLATE_KEY = "chat_template"
|
||||
_KNOWN_KEYS = (
|
||||
_DESCRIPTION_KEY,
|
||||
_SOURCE_KEY,
|
||||
_PROMPT_INPUT_KEY,
|
||||
_PROMPT_NO_INPUT_KEY,
|
||||
_RESPONSE_SEPARATOR,
|
||||
_INSTRUCTION_SEPARATOR,
|
||||
_CHAT_TEMPLATE_KEY,
|
||||
)
|
||||
|
||||
|
||||
def is_gcs_path(input_path: str) -> bool:
|
||||
"""Checks if the input path is a Google Cloud Storage (GCS) path.
|
||||
|
||||
Args:
|
||||
input_path: The input path to be checked.
|
||||
|
||||
Returns:
|
||||
True if the input path is a GCS path, False otherwise.
|
||||
"""
|
||||
return input_path is not None and input_path.startswith(GCS_URI_PREFIX)
|
||||
|
||||
|
||||
def force_gcs_fuse_path(gcs_uri: str) -> str:
|
||||
"""Converts gs:// uris to their /gcs/ equivalents. No-op for other uris.
|
||||
|
||||
Args:
|
||||
gcs_uri: The GCS URI to convert.
|
||||
|
||||
Returns:
|
||||
The converted GCS URI.
|
||||
"""
|
||||
if is_gcs_path(gcs_uri):
|
||||
return GCSFUSE_URI_PREFIX + gcs_uri[len(GCS_URI_PREFIX) :]
|
||||
else:
|
||||
return gcs_uri
|
||||
|
||||
|
||||
def download_gcs_uri_to_local(
|
||||
gcs_uri: str, destination_dir: str = LOCAL_BASE_MODEL_DIR
|
||||
) -> str:
|
||||
"""Downloads GCS URI to local.
|
||||
|
||||
If GCS URI is a directory, gs://some/folder is downloaded to
|
||||
/destination_dir/folder. If GCS URI is a file, gs://some/file is downloaded to
|
||||
/destination_dir/file.
|
||||
|
||||
Args:
|
||||
gcs_uri: GCS URI to download.
|
||||
destination_dir: Local directory directory.
|
||||
|
||||
Returns:
|
||||
Local path to target folder/file.
|
||||
"""
|
||||
target = os.path.join(
|
||||
destination_dir,
|
||||
os.path.basename(os.path.normpath(gcs_uri)),
|
||||
)
|
||||
if os.path.exists(target):
|
||||
logging.info("File %s already exists.", target)
|
||||
return target
|
||||
if accelerate.PartialState().is_local_main_process:
|
||||
logging.info(
|
||||
"Downloading file(s) from %s to %s...", gcs_uri, destination_dir
|
||||
)
|
||||
if not os.path.exists(destination_dir):
|
||||
os.mkdir(destination_dir)
|
||||
subprocess.check_output([
|
||||
"gsutil",
|
||||
"-m",
|
||||
"cp",
|
||||
"-r",
|
||||
gcs_uri,
|
||||
destination_dir,
|
||||
])
|
||||
logging.info("Downloaded file(s) from %s to %s.", gcs_uri, destination_dir)
|
||||
# Make sure ALL processes process to next step after data downloading is done.
|
||||
# It matters for the main process to wait for other processes as well.
|
||||
accelerate.PartialState().wait_for_everyone()
|
||||
return target
|
||||
|
||||
|
||||
def get_template(template_path: str) -> Dict[str, str]:
|
||||
"""Gets the template dictionary given the file path.
|
||||
|
||||
Args:
|
||||
template_path: Path to the template file.
|
||||
|
||||
Returns:
|
||||
A dictionary of the template.
|
||||
|
||||
Raises:
|
||||
ValueError: If the template file does not exist or contains unknown keys.
|
||||
"""
|
||||
if is_gcs_path(template_path):
|
||||
template_path = force_gcs_fuse_path(template_path)
|
||||
elif not os.path.isfile(template_path):
|
||||
template_path = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
_TEMPLATE_DIRNAME,
|
||||
template_path + ".json",
|
||||
)
|
||||
if not os.path.isfile(template_path):
|
||||
raise ValueError(f"Template file {template_path} does not exist.")
|
||||
with open(template_path, "r") as f:
|
||||
template_json: dict[str, str] = json.load(f)
|
||||
for key in template_json:
|
||||
if key not in _KNOWN_KEYS:
|
||||
raise ValueError(f"Unknown key {key} in template {template_path}.")
|
||||
return template_json
|
||||
|
||||
|
||||
def get_response_separator(template_json: Dict[str, str]) -> Union[str, None]:
|
||||
return template_json.get(_RESPONSE_SEPARATOR, None)
|
||||
|
||||
|
||||
def get_instruction_separator(
|
||||
template_json: Dict[str, str],
|
||||
) -> Union[str, None]:
|
||||
return template_json.get(_INSTRUCTION_SEPARATOR, None)
|
||||
|
||||
|
||||
def _format_template_fn(
|
||||
template: str,
|
||||
input_column: str,
|
||||
tokenizer: transformers.PreTrainedTokenizer | None = None,
|
||||
) -> Callable[[Dict[str, str]], Dict[str, str]]:
|
||||
"""Formats a dataset example according to a template.
|
||||
|
||||
Args:
|
||||
template: Name of the JSON template file under `templates/` or GCS path to
|
||||
the template file.
|
||||
input_column: The input column in the dataset to be used or updated by the
|
||||
template. If it does not exist, the template's `prompt_no_input` will be
|
||||
used, and the input_column will be created.
|
||||
tokenizer: The tokenizer to use for chat_template templates.
|
||||
|
||||
Returns:
|
||||
A function that formats data according to the template.
|
||||
"""
|
||||
template_json = get_template(template)
|
||||
|
||||
if _CHAT_TEMPLATE_KEY not in template_json:
|
||||
|
||||
def format_fn(example: Dict[str, str]) -> Dict[str, str]:
|
||||
format_dict = {key: value for key, value in example.items()}
|
||||
format_str = (
|
||||
template_json[_PROMPT_INPUT_KEY]
|
||||
if format_dict.get(input_column)
|
||||
else template_json[_PROMPT_NO_INPUT_KEY]
|
||||
)
|
||||
return {input_column: format_str.format(**format_dict)}
|
||||
|
||||
return format_fn
|
||||
elif (
|
||||
_PROMPT_INPUT_KEY in template_json
|
||||
or _PROMPT_NO_INPUT_KEY in template_json
|
||||
):
|
||||
raise ValueError(
|
||||
"chat_template templates do not support input/no_input templates."
|
||||
)
|
||||
else:
|
||||
if tokenizer is None:
|
||||
raise ValueError("A tokenizer is required for chat_template templates.")
|
||||
# Assign HuggingFace jinja template.
|
||||
tokenizer.chat_template = template_json[_CHAT_TEMPLATE_KEY]
|
||||
return lambda example: {
|
||||
input_column: tokenizer.apply_chat_template(
|
||||
example[input_column], tokenize=False, add_generation_prompt=False
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def _format_template_fn_notebook(
|
||||
template: str,
|
||||
input_column: str,
|
||||
tokenizer: transformers.PreTrainedTokenizer | None = None,
|
||||
) -> Callable[[Dict[str, str]], Dict[str, str]]:
|
||||
"""Formats a dataset example according to a template.
|
||||
|
||||
Args:
|
||||
template: Name of the JSON template file under `templates/` or GCS path to
|
||||
the template file.
|
||||
input_column: The input column in the dataset to be used or updated by the
|
||||
template. If it does not exist, the template's `prompt_no_input` will be
|
||||
used, and the input_column will be created.
|
||||
tokenizer: The tokenizer to use for chat_template templates.
|
||||
|
||||
Returns:
|
||||
A function that formats data according to the template.
|
||||
"""
|
||||
template_json = get_template(template)
|
||||
|
||||
if _CHAT_TEMPLATE_KEY not in template_json:
|
||||
|
||||
def format_fn(example: Dict[str, str]) -> Dict[str, str]:
|
||||
format_dict = {key: value for key, value in example.items()}
|
||||
if format_dict.get(input_column):
|
||||
format_str = template_json[_PROMPT_INPUT_KEY]
|
||||
elif _PROMPT_NO_INPUT_KEY in template_json:
|
||||
format_str = template_json[_PROMPT_NO_INPUT_KEY]
|
||||
else:
|
||||
raise KeyError(
|
||||
f"The template {os.path.basename(template)} does not contain"
|
||||
f" {_PROMPT_INPUT_KEY} or {_PROMPT_NO_INPUT_KEY} key."
|
||||
)
|
||||
try:
|
||||
return {input_column: format_str.format(**format_dict)}
|
||||
except KeyError as e:
|
||||
raise KeyError(
|
||||
f"The template {os.path.basename(template)} contains a key {e} in"
|
||||
f" {_PROMPT_INPUT_KEY} or {_PROMPT_NO_INPUT_KEY} that does not"
|
||||
" exist in the dataset example. The dataset example looks like"
|
||||
f" {format_dict}."
|
||||
) from e
|
||||
|
||||
return format_fn
|
||||
elif (
|
||||
_PROMPT_INPUT_KEY in template_json
|
||||
or _PROMPT_NO_INPUT_KEY in template_json
|
||||
):
|
||||
raise ValueError(
|
||||
f"chat_template templates do not support {_PROMPT_INPUT_KEY} or"
|
||||
f" {_PROMPT_NO_INPUT_KEY} templates."
|
||||
)
|
||||
else:
|
||||
if tokenizer is None:
|
||||
raise ValueError("A tokenizer is required for chat_template templates.")
|
||||
# Assign HuggingFace jinja template.
|
||||
tokenizer.chat_template = template_json[_CHAT_TEMPLATE_KEY]
|
||||
|
||||
def format_fn(example: Dict[str, str]) -> Dict[str, str]:
|
||||
try:
|
||||
return {
|
||||
input_column: tokenizer.apply_chat_template(
|
||||
example[input_column],
|
||||
tokenize=False,
|
||||
add_generation_prompt=False,
|
||||
)
|
||||
}
|
||||
except KeyError as e:
|
||||
raise KeyError(
|
||||
f"The template {os.path.basename(template)} contains a key {e} in"
|
||||
f" {_CHAT_TEMPLATE_KEY} that does not exist in the dataset example."
|
||||
) from e
|
||||
|
||||
return format_fn
|
||||
|
||||
|
||||
def _get_split_string(
|
||||
split: str,
|
||||
dataset_percent: int | None = None,
|
||||
dataset_k_rows: int | None = None,
|
||||
) -> str:
|
||||
"""Gets the formatted split string for the dataset.
|
||||
|
||||
This is used to format the split string as per
|
||||
https://huggingface.co/docs/datasets/v2.21.0/loading#slice-splits. Also, this
|
||||
function will only be used to load the partial dataset for validating the
|
||||
dataset against the template.
|
||||
|
||||
Args:
|
||||
split: Split of the dataset.
|
||||
dataset_percent: The percentage of the dataset to load.
|
||||
dataset_k_rows: The top k sequences to load from the dataset.
|
||||
|
||||
Returns:
|
||||
A formatted split string.
|
||||
"""
|
||||
# Validate the dataset_percent and dataset_k_rows values.
|
||||
if dataset_percent and dataset_k_rows:
|
||||
raise ValueError(
|
||||
"You can set either validate_percentage_of_dataset or"
|
||||
" validate_k_rows_of_dataset, but not both."
|
||||
)
|
||||
|
||||
if dataset_percent:
|
||||
logging.info("Loading %d percent of the dataset...", dataset_percent)
|
||||
return f"{split}[:{dataset_percent}%]"
|
||||
|
||||
if dataset_k_rows:
|
||||
logging.info("Loading top %d rows of the dataset...", dataset_k_rows)
|
||||
return f"{split}[:{dataset_k_rows}]"
|
||||
|
||||
return split
|
||||
|
||||
|
||||
def _github_template_path(template: str) -> str:
|
||||
"""Generates the path to the template in the Vertex AI Samples GitHub repo.
|
||||
|
||||
Args:
|
||||
template: Name of the template.
|
||||
|
||||
Returns:
|
||||
The path to the template in the Vertex AI Samples GitHub repo.
|
||||
"""
|
||||
# vertex-ai-samples directory may lie under separate directory depending on
|
||||
# the scratch_dir parameter in the notebook execution environment.
|
||||
vertex_ai_samples_abs_path = os.getcwd().split(
|
||||
_VERTEX_AI_SAMPLES_GITHUB_REPO_NAME
|
||||
)[0]
|
||||
return os.path.join(
|
||||
vertex_ai_samples_abs_path,
|
||||
_VERTEX_AI_SAMPLES_GITHUB_REPO_NAME,
|
||||
_VERTEX_AI_SAMPLES_GITHUB_TEMPLATE_DIR,
|
||||
template + ".json",
|
||||
)
|
||||
|
||||
|
||||
def _get_dataset(
|
||||
dataset_name: str,
|
||||
split: str,
|
||||
num_proc: int | None = None,
|
||||
) -> datasets.DatasetDict:
|
||||
"""Gets a dataset.
|
||||
|
||||
Args:
|
||||
dataset_name: Name of the dataset or path to a custom dataset.
|
||||
split: Split of the dataset.
|
||||
num_proc: Number of processors to use.
|
||||
|
||||
Returns:
|
||||
A dataset.
|
||||
"""
|
||||
dataset_name = force_gcs_fuse_path(dataset_name)
|
||||
if os.path.isfile(dataset_name):
|
||||
# Custom dataset.
|
||||
return datasets.load_dataset(
|
||||
"json",
|
||||
data_files=[dataset_name],
|
||||
split=split,
|
||||
num_proc=num_proc,
|
||||
)
|
||||
# HF dataset.
|
||||
return datasets.load_dataset(dataset_name, split=split, num_proc=num_proc)
|
||||
|
||||
|
||||
def load_dataset_with_template(
|
||||
dataset_name: str,
|
||||
split: str,
|
||||
input_column: str,
|
||||
template: str = None,
|
||||
tokenizer: transformers.PreTrainedTokenizer | None = None,
|
||||
) -> Any:
|
||||
"""Loads dataset with templates.
|
||||
|
||||
Args:
|
||||
dataset_name: Name of the dataset or path to a custom dataset.
|
||||
split: Split of the dataset.
|
||||
input_column: The input column in the dataset to be used or updaded by the
|
||||
template. If it does not exist, the template's `prompt_no_input` will be
|
||||
used, and the input_column will be created.
|
||||
template: Name of the JSON template file under `templates/` or GCS path to
|
||||
the template file.
|
||||
tokenizer: The tokenizer to use for chat_template templates.
|
||||
|
||||
Returns:
|
||||
A dataset compatible with the template.
|
||||
"""
|
||||
dataset = _get_dataset(dataset_name, split=split)
|
||||
if template:
|
||||
dataset = dataset.map(
|
||||
_format_template_fn(
|
||||
template,
|
||||
input_column=input_column,
|
||||
tokenizer=tokenizer,
|
||||
)
|
||||
)
|
||||
|
||||
return dataset
|
||||
|
||||
|
||||
def validate_dataset_with_template(
|
||||
dataset_name: str,
|
||||
split: str,
|
||||
input_column: str,
|
||||
template: str,
|
||||
tokenizer: transformers.PreTrainedTokenizer | None = None,
|
||||
use_multiprocessing: bool = False,
|
||||
validate_percentage_of_dataset: int | None = None,
|
||||
validate_k_rows_of_dataset: int | None = None,
|
||||
) -> Any:
|
||||
"""Validates dataset with templates.
|
||||
|
||||
This function will be used to load the dataset and validate it against the
|
||||
template. In case of validation, we also allow the users to load the dataset
|
||||
partially by allowing them to read x% or top k rows of the dataset. To
|
||||
validate the dataset, the template file must be available in the GCS bucket
|
||||
and the dataset must be available either in the GCS bucket or Hugging Face.
|
||||
|
||||
Args:
|
||||
dataset_name: Name of the dataset or path to a custom dataset.
|
||||
split: Split of the dataset.
|
||||
input_column: The input column in the dataset to be used or updaded by the
|
||||
template. If it does not exist, the template's `prompt_no_input` will be
|
||||
used, and the input_column will be created.
|
||||
template: Name of the JSON template file under `templates/` or GCS path to
|
||||
the template file.
|
||||
tokenizer: The tokenizer to use for chat_template templates.
|
||||
use_multiprocessing: If True, it will use multiprocessing to load the
|
||||
dataset.
|
||||
validate_percentage_of_dataset: The percentage of the dataset to load.
|
||||
validate_k_rows_of_dataset: The top k sequences to load from the dataset.
|
||||
|
||||
Returns:
|
||||
None if the validation is successful, otherwise returns the error message.
|
||||
"""
|
||||
if not template:
|
||||
raise ValueError("template is required for validate_dataset.")
|
||||
|
||||
if not dataset_name:
|
||||
raise ValueError("dataset_name is empty.")
|
||||
|
||||
if not split:
|
||||
raise ValueError("split is empty.")
|
||||
|
||||
split = _get_split_string(
|
||||
split,
|
||||
validate_percentage_of_dataset,
|
||||
validate_k_rows_of_dataset,
|
||||
)
|
||||
|
||||
num_proc = multiprocessing.cpu_count() if use_multiprocessing else 1
|
||||
|
||||
# gcsfuse cannot be used from the notebook runtime env. Hence, we have
|
||||
# to download dataset and template from gcs to local.
|
||||
if is_gcs_path(dataset_name):
|
||||
dataset_name = download_gcs_uri_to_local(dataset_name, LOCAL_BASE_MODEL_DIR)
|
||||
|
||||
if is_gcs_path(template):
|
||||
template_path = download_gcs_uri_to_local(template, LOCAL_TEMPLATE_DIR)
|
||||
elif os.path.isfile(_github_template_path(template)):
|
||||
template_path = _github_template_path(template)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Template file {template} does not exist. To validate the"
|
||||
" dataset, please provide a valid GCS path for the template or a valid"
|
||||
" template name from"
|
||||
f" https://github.com/GoogleCloudPlatform/{_VERTEX_AI_SAMPLES_GITHUB_REPO_NAME}/tree/main/{_VERTEX_AI_SAMPLES_GITHUB_TEMPLATE_DIR}."
|
||||
)
|
||||
|
||||
_get_dataset(dataset_name, split, num_proc).map(
|
||||
_format_template_fn_notebook(
|
||||
template_path,
|
||||
input_column=input_column,
|
||||
tokenizer=tokenizer,
|
||||
)
|
||||
)
|
||||
|
||||
print(
|
||||
"Dataset {} is compatible with the {} template.".format(
|
||||
os.path.basename(dataset_name), os.path.basename(template)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# Vertex Model Garden Training Dataset Template
|
||||
|
||||
## Overview
|
||||
|
||||
Vertex Model Garden training provides templates for streamlined preprocessing of
|
||||
datasets. Although datasets often have intricate structures, the supported LLM
|
||||
models accept only flat strings. A template facilitates parsing a dataset and
|
||||
preprocessing it to be compatible with the model.
|
||||
|
||||
When fine-tuning a pretrained model, it is advisable to maintain the same format
|
||||
as the original training data. A template helps replicate the format, ensuring
|
||||
consistency and potentially enhancing the fine-tuning process.
|
||||
|
||||
Both multi-turn messages and single instruction-response pairs are supported.
|
||||
Multi-turn messages are accommodated using a more general `chat_template` field,
|
||||
whereas simple instruction-response pair datasets are supported through the
|
||||
`prompt_input` field.
|
||||
|
||||
A template is a JSON file consisting of string key-value pairs. Refer to the
|
||||
following for the definitions of the supported fields.
|
||||
|
||||
## Template field documentation
|
||||
|
||||
**description**: An explanation of the template.
|
||||
|
||||
**source**: Information about the origin of the template.
|
||||
|
||||
**chat_template**: A
|
||||
[jinja template](https://jinja.palletsprojects.com/en/3.1.x/templates/) that can
|
||||
be used to parse a chat dataset. This is the same format as
|
||||
[HF chat templates](https://huggingface.co/docs/transformers/main/en/chat_templating).
|
||||
To create a chat_template, use the `messages` variable to be filled with the
|
||||
sample. The flag `--instruct_column_in_dataset` identifies which column will be
|
||||
passed to the `messages` variable in the chat_template. This field is mutually
|
||||
exclusive with `prompt_input` and `prompt_no_input`.
|
||||
|
||||
**prompt_input**: A string template that is used when value for the input column
|
||||
exists in the sample. It should be able to be formatted with the
|
||||
[str.format](https://docs.python.org/3/library/stdtypes.html#str.format) method.
|
||||
The input column is specified with the flag `--instruct_column_in_dataset`. Used
|
||||
for instruction dataset. This field is mutually exclusive with `chat_template`.
|
||||
|
||||
**prompt_no_input**: A string template that is used when value for the input
|
||||
column does not exist in the sample. It should be able to be formatted with the
|
||||
[str.format](https://docs.python.org/3/library/stdtypes.html#str.format) method.
|
||||
The input column is specified with the flag `--instruct_column_in_dataset`. Used
|
||||
for instruction dataset. This field is mutually exclusive with `chat_template`.
|
||||
|
||||
**instruction_separator**: A unique string used to indicate the start of the
|
||||
instructions. If not specified, every token after response_separator will be
|
||||
treated as a response, and every token before the first response_separator will
|
||||
be treated as instruction.
|
||||
|
||||
**response_separator**: A unique string used to indicate the start of the
|
||||
response. This field is required if `--completion_only` flag is set to `True`.
|
||||
|
||||
## Example templates
|
||||
|
||||
- See the list of all supported templates [here](https://github.com/GoogleCloudPlatform/vertex-ai-samples/tree/main/community-content/vertex_model_garden/model_oss/peft/train/vmg/templates).
|
||||
- For an example with `chat_template` see the JSON template below.
|
||||
|
||||
```
|
||||
{
|
||||
"description": "Chat template used by Llama 3.",
|
||||
"source": "https://huggingface.co/meta-llama/Meta-Llama-3-70B-Instruct/blob/a5a71a7527eac1d651bb145436c72026887fb68e/tokenizer_config.json#L2053",
|
||||
"chat_template": "{% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' %}{% if loop.index0 == 0 %}{% set content = bos_token + content %}{% endif %}{{ content }}{% endfor %}{% if add_generation_prompt %}{{ '<|start_header_id|>assistant<|end_header_id|>\n\n' }}{% endif %}",
|
||||
"instruction_separator": "<|start_header_id|>user<|end_header_id|>\n\n",
|
||||
"response_separator": "<|start_header_id|>assistant<|end_header_id|>\n\n"
|
||||
}
|
||||
```
|
||||
|
||||
- For an example with `prompt_input` see the JSON template below. In this case
|
||||
the flag `--instruct_column_in_dataset=text` should be set, and there must
|
||||
be a column named `text` in the dataset.
|
||||
|
||||
```
|
||||
{
|
||||
"description": "Template for openassistant-guanaco dataset.",
|
||||
"source": "https://huggingface.co/datasets/timdettmers/openassistant-guanaco",
|
||||
"prompt_input": "{text}",
|
||||
"instruction_separator": "### Human:",
|
||||
"response_separator": "### Assistant:"
|
||||
}
|
||||
```
|
||||
|
||||
- For an example with `prompt_no_input` see the JSON template below. In this
|
||||
case the flag `--instruct_column_in_dataset=input` should be set, and there
|
||||
must be columns named `input` and `instruction` in the dataset.
|
||||
|
||||
```
|
||||
{
|
||||
"description": "Template used by Alpaca-LoRA.",
|
||||
"source": "https://github.com/tloen/alpaca-lora/blob/main/templates/alpaca.json",
|
||||
"prompt_input": "Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.\n\n### Instruction:\n{instruction}\n\n### Input:\n{input}\n\n### Response:\n",
|
||||
"prompt_no_input": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\n{instruction}\n\n### Response:\n",
|
||||
"response_separator": "### Response:"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"description": "Template used by Alpaca-LoRA.",
|
||||
"source": "https://github.com/tloen/alpaca-lora/blob/main/templates/alpaca.json",
|
||||
"prompt_input": "Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.\n\n### Instruction:\n{instruction}\n\n### Input:\n{input}\n\n### Response:\n",
|
||||
"prompt_no_input": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\n{instruction}\n\n### Response:\n",
|
||||
"response_separator": "### Response:"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"description": "A shorter template to experiment with.",
|
||||
"source": "https://github.com/tloen/alpaca-lora/blob/main/templates/alpaca_short.json",
|
||||
"prompt_input": "### Instruction:\n{instruction}\n\n### Input:\n{input}\n\n### Response:\n",
|
||||
"prompt_no_input": "### Instruction:\n{instruction}\n\n### Response:\n",
|
||||
"response_separator": "### Response:"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"description": "Chat template used by Gemma. 'assistant' role is replaced by 'model'",
|
||||
"source": "https://huggingface.co/google/gemma-1.1-2b-it/blob/bf4924f313df5166dee1467161e886e55f2eb4d4/tokenizer_config.json#L1507",
|
||||
"chat_template": "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{{ raise_exception('System role not supported') }}{% endif %}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if (message['role'] == 'assistant') %}{% set role = 'model' %}{% else %}{% set role = message['role'] %}{% endif %}{{ '<start_of_turn>' + role + '\n' + message['content'] | trim + '<end_of_turn>\n' }}{% endfor %}{% if add_generation_prompt %}{{'<start_of_turn>model\n'}}{% endif %}",
|
||||
"instruction_separator": "<start_of_turn>user\n",
|
||||
"response_separator": "<start_of_turn>model\n"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"description": "Template used by Llama 3, accepting text-bison format.",
|
||||
"source": "https://cloud.google.com/vertex-ai/generative-ai/docs/models/tune-text-models-supervised#dataset-format",
|
||||
"prompt_input": "\n\n<|start_header_id|>user<|end_header_id|>\n\n{input_text}<|eot_id|>\n\n<|start_header_id|>assistant<|end_header_id|>\n\n{output_text}<|eot_id|>",
|
||||
"instruction_separator": "<|start_header_id|>user<|end_header_id|>\n\n",
|
||||
"response_separator": "<|start_header_id|>assistant<|end_header_id|>\n\n"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"description": "Chat template used by Llama 3.",
|
||||
"source": "https://huggingface.co/meta-llama/Meta-Llama-3-70B-Instruct/blob/a5a71a7527eac1d651bb145436c72026887fb68e/tokenizer_config.json#L2053",
|
||||
"chat_template": "{% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' %}{% if loop.index0 == 0 %}{% set content = bos_token + content %}{% endif %}{{ content }}{% endfor %}{% if add_generation_prompt %}{{ '<|start_header_id|>assistant<|end_header_id|>\n\n' }}{% endif %}",
|
||||
"instruction_separator": "<|start_header_id|>user<|end_header_id|>\n\n",
|
||||
"response_separator": "<|start_header_id|>assistant<|end_header_id|>\n\n"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"description": "Chat template used by Mistral.",
|
||||
"source": "https://github.com/OpenAccess-AI-Collective/axolotl/blob/main/src/axolotl/utils/chat_templates.py",
|
||||
"chat_template": "{{ bos_token }}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST] ' + message['content'] + ' [/INST]' }}{% elif message['role'] == 'assistant' %}{{ message['content'] + eos_token}}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}",
|
||||
"instruction_separator": "[INST]",
|
||||
"response_separator": "[/INST]"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"description": "Template used by openai chat.",
|
||||
"source": "https://platform.openai.com/docs/api-reference/fine-tuning/chat-input",
|
||||
"chat_template": "{% set loop_messages = messages %}{% set content = '' %}{% for message in loop_messages %}{% set content = content ~ '\n\n<|start_header_id|>' ~ message.role ~ '<|end_header_id|>\n\n' %}{% if message.content is string %}{% set content = content ~ message.content|trim ~ '<|eot_id|>' %}{% else %}{% set content = content ~ message.content|join(' ', attribute='text')|trim ~ '<|eot_id|>' %}{% endif %}{% if loop.index0 == 0 %}{% set content = bos_token ~ content %}{% endif %}{{ content }}{% endfor %}{% if add_generation_prompt %}{{ '<|start_header_id|>assistant<|end_header_id|>\n\n' }}{% endif %}",
|
||||
"instruction_separator": "<|start_header_id|>user<|end_header_id|>\n\n",
|
||||
"response_separator": "<|start_header_id|>assistant<|end_header_id|>\n\n"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"description": "Template used by openai completion.",
|
||||
"source": "https://platform.openai.com/docs/api-reference/fine-tuning/completions-input",
|
||||
"prompt_input": "\n\n<|start_header_id|>user<|end_header_id|>\n\n{prompt}<|eot_id|>\n\n<|start_header_id|>assistant<|end_header_id|>\n\n{completion}<|eot_id|>",
|
||||
"instruction_separator": "<|start_header_id|>user<|end_header_id|>\n\n",
|
||||
"response_separator": "<|start_header_id|>assistant<|end_header_id|>\n\n"
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"description": "Template for openassistant-guanaco dataset.",
|
||||
"source": "https://huggingface.co/datasets/timdettmers/openassistant-guanaco",
|
||||
"prompt_input": "{text}",
|
||||
"instruction_separator": "### Human:",
|
||||
"response_separator": "### Assistant:"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"description": "Template used for chat based models.",
|
||||
"source": "https://huggingface.co/meta-llama/Meta-Llama-3-70B-Instruct/blob/a5a71a7527eac1d651bb145436c72026887fb68e/tokenizer_config.json#L2053",
|
||||
"chat_template": "{% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' %}{% if loop.index0 == 0 %}{% set content = bos_token + content %}{% endif %}{{ content }}{% endfor %}{% if add_generation_prompt %}{{ '<|start_header_id|>model<|end_header_id|>\n\n' }}{% endif %}",
|
||||
"instruction_separator": "<|start_header_id|>user<|end_header_id|>\n\n",
|
||||
"response_separator": "<|start_header_id|>model<|end_header_id|>\n\n"
|
||||
}
|
||||
Reference in New Issue
Block a user